diff --git a/browser/src/canvas/sections/CommentSection.ts b/browser/src/canvas/sections/CommentSection.ts index 4604b92926386..121fcbe72a03b 100644 --- a/browser/src/canvas/sections/CommentSection.ts +++ b/browser/src/canvas/sections/CommentSection.ts @@ -123,6 +123,7 @@ export class Comment extends CanvasSectionObject { this.sectionProperties.acceptButton = null; this.sectionProperties.rejectButton = null; this.sectionProperties.menu = null; + this.sectionProperties.menuBarCell = null; this.sectionProperties.captionNode = null; this.sectionProperties.captionText = null; @@ -228,7 +229,11 @@ export class Comment extends CanvasSectionObject { this.createTrackChangeButtons(); } - if (this.sectionProperties.noMenu !== true && app.isCommentEditingAllowed()) { + // Always create the menu if allowed; its visibility is kept in sync with + // the current edit permission by updateEditability() so the Viewing/ + // Editing toggle can show or hide the Edit/Reply/Delete affordances + // without re-rendering the comment. + if (this.sectionProperties.noMenu !== true) { this.createMenu(); } @@ -273,6 +278,35 @@ export class Comment extends CanvasSectionObject { if (!(window).mode.isMobile()) document.getElementById('document-container').appendChild(this.sectionProperties.container); + + this.updateEditability(); + this.sectionProperties.onUpdatePermissionBound = this.updateEditability.bind(this); + app.events.on('updatepermission', this.sectionProperties.onUpdatePermissionBound); + } + + // Syncs visible edit affordances with the current comment-editing + // permission. The editable=true branch only re-shows the menubar + // cell: nodeModify/nodeReply remain hidden until the user explicitly + // triggers edit()/reply(), so we deliberately do NOT auto-reopen a + // previously open edit/reply pane when permission is restored. + private makeEditable (editable: boolean): void { + const props = this.sectionProperties; + if (props.menuBarCell?.style) + props.menuBarCell.style.display = editable ? '' : 'none'; + if (editable) return; + if (props.nodeModify?.style) props.nodeModify.style.display = 'none'; + if (props.nodeReply?.style) props.nodeReply.style.display = 'none'; + if (props.contentNode?.style) props.contentNode.style.display = ''; + props.container.classList.remove('modify-annotation-container'); + props.container.classList.remove('reply-annotation-container'); + this.cachedIsEdit = false; + } + + // Invoked at init and on every updatepermission event, so toggling + // between Viewing and Editing mode immediately hides or restores the + // per-comment controls. + private updateEditability (): void { + this.makeEditable(app.isCommentEditingAllowed()); } private createContainerAndWrapper (): void { @@ -331,6 +365,7 @@ export class Comment extends CanvasSectionObject { private createMenu (): void { var tdMenu = window.L.DomUtil.create('td', 'cool-annotation-menubar', this.sectionProperties.authorRow); + this.sectionProperties.menuBarCell = tdMenu; const edit = window.L.DomUtil.create('div', 'cool-annotation-menu-edit', tdMenu); edit.id = 'comment-annotation-menu-edit-' + this.sectionProperties.data.id; edit.tabIndex = 0; @@ -1703,6 +1738,11 @@ export class Comment extends CanvasSectionObject { public onRemove (): void { this.sectionProperties.commentContainerRemoved = true; + if (this.sectionProperties.onUpdatePermissionBound) { + app.events.off('updatepermission', this.sectionProperties.onUpdatePermissionBound); + this.sectionProperties.onUpdatePermissionBound = null; + } + if (this.sectionProperties.commentListSection.sectionProperties.selectedComment === this) this.sectionProperties.commentListSection.sectionProperties.selectedComment = null; diff --git a/browser/src/control/Permission.js b/browser/src/control/Permission.js index 52e7d7fe3d5d8..538035504c861 100644 --- a/browser/src/control/Permission.js +++ b/browser/src/control/Permission.js @@ -210,6 +210,22 @@ window.L.Map.include({ } }, + // Tell core whether this view is read-only, and flip the client-side + // comment/redline gates the UI checks via isCommentEditingAllowed()/ + // isRedlineManagementAllowed(). Only meaningful for users that actually + // have WOPI write permission: users without write permission already + // get read-only set up server-side at session start, and their + // app.file.editComment / allowManageRedlines flags already reflect the + // real doc state (e.g. comment-only PDFs) and must not be touched. + _applyViewReadOnly: function (readOnly) { + if (!this['wopi'] || !this['wopi'].UserCanWrite) + return; + if (app.socket) + app.socket.sendMessage('setviewreadonly value=' + readOnly); + app.file.editComment = !readOnly; + app.file.allowManageRedlines = !readOnly; + }, + _enterEditMode: function (perm) { this._permission = perm; @@ -220,6 +236,11 @@ window.L.Map.include({ if (app.map['stateChangeHandler'].getItemValue('EditDoc') === 'false') app.map.sendUnoCommand('.uno:EditDoc?Editable:bool=true'); + // Re-enable direct-canvas interactions (shape drag, arrow-key + // shape move) that the matching _enterReadOnlyMode branch + // disabled. + this._applyViewReadOnly(false); + app.events.fire('updatepermission', {perm : perm}); if (this._docLayer._docType === 'text') { @@ -241,6 +262,12 @@ window.L.Map.include({ this._docLayer._onUpdateCursor(); this._docLayer._clearSelections(); } + + // Block direct-canvas interactions (shape drag, arrow-key shape + // move) server-side and hide per-comment edit/redline controls + // in the UI. + this._applyViewReadOnly(true); + app.events.fire('updatepermission', {perm : perm}); this.fire('closemobilewizard'); this.fire('closealldialogs'); diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp index d7577becb0c49..1f382dc1c6a35 100644 --- a/kit/ChildSession.cpp +++ b/kit/ChildSession.cpp @@ -578,6 +578,7 @@ bool ChildSession::_handleInput(const char *buffer, int length) tokens.equals(0, "formfieldevent") || tokens.equals(0, "traceeventrecording") || tokens.equals(0, "sallogoverride") || + tokens.equals(0, "setviewreadonly") || tokens.equals(0, "rendersearchresult") || tokens.equals(0, "contentcontrolevent") || tokens.equals(0, "a11ystate") || @@ -848,6 +849,33 @@ bool ChildSession::_handleInput(const char *buffer, int length) getLOKit()->setOption("sallogoverride", tokens[1].c_str()); } } + else if (tokens.equals(0, "setviewreadonly")) + { + // Propagate the browser-side Viewing/Editing toggle to core so it can + // block direct-canvas interactions (shape drag, arrow-key move) and + // gate comment/redline commands via the dispatch filter. + bool readOnly = false; + std::string value; + if (tokens.size() > 1 && getTokenString(tokens[1], "value", value)) + readOnly = (value == "true"); + + if (getLOKitDocument()) + { + getLOKitDocument()->setView(_viewId); + getLOKitDocument()->setViewReadOnly(_viewId, readOnly); + + // Browser only sends setviewreadonly when the user has WOPI + // write permission, so this path is the Viewing/Editing toggle + // on a fully editable doc. Block comments and redline management + // in Viewing mode too - comment-only docs (e.g. PDFs) are set + // up separately at session start and never reach this branch. + getLOKitDocument()->setAllowChangeComments(_viewId, !readOnly); + getLOKitDocument()->setAllowManageRedlines(_viewId, !readOnly); + + LOG_DBG("setviewreadonly: viewId=" << _viewId + << " readOnly=" << readOnly); + } + } else if (tokens.equals(0, "rendersearchresult")) { return renderSearchResult(buffer, length, tokens); diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index 9b69550aa9acc..f15c159278ca3 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -1234,6 +1234,7 @@ bool ClientSession::_handleInput(const char *buffer, int length) } else if (tokens.equals(0, "formfieldevent") || tokens.equals(0, "sallogoverride") || + tokens.equals(0, "setviewreadonly") || tokens.equals(0, "contentcontrolevent")) { return forwardToChild(firstLine, docBroker);