diff --git a/frontend/src/pages/lib-content-view/lib-content-view.js b/frontend/src/pages/lib-content-view/lib-content-view.js index 8fc6aec61d..01f81b4c18 100644 --- a/frontend/src/pages/lib-content-view/lib-content-view.js +++ b/frontend/src/pages/lib-content-view/lib-content-view.js @@ -32,7 +32,7 @@ import DirColumnView from '../../components/dir-view-mode/dir-column-view'; import SelectedDirentsToolbar from '../../components/toolbar/selected-dirents-toolbar'; import MetadataPathToolbar from '../../components/toolbar/metadata-path-toolbar'; import { eventBus } from '../../components/common/event-bus'; - +import WebSocketClient from '../../utils/websocket-service'; import '../../css/lib-content-view.css'; dayjs.extend(relativeTime); @@ -49,7 +49,6 @@ class LibContentView extends React.Component { constructor(props) { super(props); - let isTreePanelShown = true; const storedTreePanelState = localStorage.getItem(TREE_PANEL_STATE_KEY); if (storedTreePanelState != undefined) { @@ -59,6 +58,7 @@ class LibContentView extends React.Component { const storedDirentDetailShowState = localStorage.getItem(DIRENT_DETAIL_SHOW_KEY); const isDirentDetailShow = storedDirentDetailShowState === 'true'; + this.socket = new WebSocketClient(this.onMessageCallback, this.props.repoID); this.state = { currentMode: cookie.load('seafile_view_mode') || LIST_MODE, isTreePanelShown: isTreePanelShown, // display the 'dirent tree' side panel @@ -161,6 +161,38 @@ class LibContentView extends React.Component { this.calculatePara(this.props); } + onMessageCallback = (data) => { + if (data.type === 'file-lock-changed') { + const currentUrl = window.location.href; + const parsedUrl = new URL(currentUrl); + const pathParts = parsedUrl.pathname.split('/').filter(part => part.length > 0); + const dirRouter = decodeURIComponent(pathParts.slice(3).join('/')); + let notiUrlIndex = ''; + if (data.content.path.includes('/')) { + notiUrlIndex = data.content.path.lastIndexOf('/'); + } + const notifRouter = data.content.path.slice(0, notiUrlIndex); + if (dirRouter === notifRouter) { + const dirent = { name: data.content.path.split('/').pop() }; + if (data.content.change_event === 'locked') { + if (data.content.expire === -1) { + this.updateDirent(dirent, 'is_freezed', true); + } else { + this.updateDirent(dirent, 'is_freezed', false); + } + this.updateDirent(dirent, 'is_locked', true); + this.updateDirent(dirent, 'locked_by_me', true); + let lockName = data.content.lock_user.split('@'); + this.updateDirent(dirent, 'lock_owner_name', lockName[0]); + } else if (data.content.change_event === 'unlocked') { + this.updateDirent(dirent, 'is_locked', false); + this.updateDirent(dirent, 'locked_by_me', false); + this.updateDirent(dirent, 'lock_owner_name', ''); + } + } + } + }; + UNSAFE_componentWillReceiveProps(nextProps) { if (nextProps.repoID !== this.props.repoID) { this.setState({ path: '/', viewId: '', tagID: '', currentMode: cookie.load('seafile_view_mode') || LIST_MODE }, () => { @@ -280,6 +312,7 @@ class LibContentView extends React.Component { isLibView: false, currentRepoInfo: null, }); + this.socket.close(); } componentDidUpdate() { @@ -405,6 +438,7 @@ class LibContentView extends React.Component { // load data loadDirData = (path) => { + // list used FileTags this.updateUsedRepoTags(); diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index c9e410a565..b3820ab9e7 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -31,6 +31,7 @@ export const fileServerRoot = window.app.config.fileServerRoot; export const useGoFileserver = window.app.config.useGoFileserver; export const seafileVersion = window.app.config.seafileVersion; export const serviceURL = window.app.config.serviceURL; +export const notificationServerUrl = window.app.config.notificationServerUrl; export const appAvatarURL = window.app.config.avatarURL; export const faviconPath = window.app.config.faviconPath; export const loginBGPath = window.app.config.loginBGPath; diff --git a/frontend/src/utils/user-api.js b/frontend/src/utils/user-api.js index 4c78bc0f8c..5296dd015c 100644 --- a/frontend/src/utils/user-api.js +++ b/frontend/src/utils/user-api.js @@ -75,6 +75,12 @@ class UserAPI { form.append('suite_id', suiteID); return this.req.put(url, form); } + + getNotificationToken(repoID) { + const url = this.server + '/api/v2.1/repos/' + repoID + '/repo-notification-jwt-token/'; + return this.req.get(url); + } + } let userAPI = new UserAPI(); diff --git a/frontend/src/utils/websocket-service.js b/frontend/src/utils/websocket-service.js new file mode 100644 index 0000000000..400ed8595a --- /dev/null +++ b/frontend/src/utils/websocket-service.js @@ -0,0 +1,116 @@ +import { userAPI } from './user-api'; +import { notificationServerUrl } from './constants'; + + +class WebSocketClient { + constructor(onMessageCallback, repoId) { + + this.url = notificationServerUrl; // WebSocket address; + this.repoId = repoId; + this.socket = null; + this.shouldReconnect = true; + this.reconnectAttempts = 0; + this.maxReconnectAttempts = 5; + this.onMessageCallback = onMessageCallback; + if (notificationServerUrl !== '') { + this.connect(); + } + } + + async connect() { + this.socket = new WebSocket(this.url); + + this.socket.onopen = async () => { + const msg = await this.formatSubscriptionMsg(); + this.socket.send(JSON.stringify(msg)); + }; + + // listen message from WebSocket server + this.socket.onmessage = async (event) => { + const parsedData = JSON.parse(event.data); + // jwt-expire reconnect + if (parsedData.type === 'jwt-expired') { + const msg = await this.formatSubscriptionMsg(); + this.socket.send(JSON.stringify(msg)); + } else { + this.onMessageCallback(parsedData); + } + }; + + this.socket.onerror = (error) => { + return error; + }; + + // reconnect WebSocket + this.socket.onclose = () => { + if (this.shouldReconnect) { + this.reconnect(); + } + }; + } + + async getRepoJwtToken() { + const response = await userAPI.getNotificationToken(this.repoId).then(res => { + return res.data; + }).catch(err => { + throw err; + }); + return response.token; + } + + async formatSubscriptionMsg() { + const repoToken = await this.getRepoJwtToken(); + const jsonData = { + type: 'subscribe', + content: { + repos: [ + { + id: this.repoId, + jwt_token: repoToken, + }, + ], + }, + }; + return jsonData; + } + + formatUnSubscriptionMsg() { + const jsonData = { + type: 'unsubscribe', + content: { + repos: [ + { + id: this.repoId + }, + ], + }, + }; + return jsonData; + } + + close() { + this.shouldReconnect = false; + if (this.socket) { + if (this.socket.readyState === WebSocket.OPEN) { + const msg = this.formatUnSubscriptionMsg(); + this.socket.send(JSON.stringify(msg)); + } + this.socket.close(); + this.socket = null; + } + + } + + reconnect() { + if (this.reconnectAttempts >= this.maxReconnectAttempts) { + return; + } + const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000); + setTimeout(() => { + this.reconnectAttempts++; + this.connect(); + }, delay); + } +} + +export default WebSocketClient; diff --git a/seahub/base/context_processors.py b/seahub/base/context_processors.py index 75889591e0..f9b55e5285 100644 --- a/seahub/base/context_processors.py +++ b/seahub/base/context_processors.py @@ -182,6 +182,7 @@ def base(request): 'enable_seafile_ai': ENABLE_SEAFILE_AI, 'enable_whiteboard': ENABLE_WHITEBOARD, 'enable_excalidraw': ENABLE_EXCALIDRAW, + 'notification_server_url': os.environ.get('NOTIFICATION_SERVER_URL', ''), } if request.user.is_staff: diff --git a/seahub/settings.py b/seahub/settings.py index 4854da4d75..859a6d50a2 100644 --- a/seahub/settings.py +++ b/seahub/settings.py @@ -941,7 +941,6 @@ SEND_EMAIL_ON_ADDING_SYSTEM_MEMBER = True # Whether to send email when a system SEND_EMAIL_ON_RESETTING_USER_PASSWD = True # Whether to send email when a system staff resetting user's password. - ########################## # Settings for seadoc # ########################## diff --git a/seahub/templates/base_for_react.html b/seahub/templates/base_for_react.html index 97f1dfbe18..3426577693 100644 --- a/seahub/templates/base_for_react.html +++ b/seahub/templates/base_for_react.html @@ -58,6 +58,7 @@ cloudMode: {% if cloud_mode %} true {% else %} false {% endif %}, isOrgContext: {% if org is not None %} true {% else %} false {% endif %}, enableSeafileAI: {% if enable_seafile_ai %} true {% else %} false {% endif %}, + notificationServerUrl: '{{ notification_server_url }}' }, pageOptions: { csrfToken: "{{ csrf_token }}",