-
-
Notifications
You must be signed in to change notification settings - Fork 5.1k
fix(comments): dismiss mention notifications and clear unread badge when viewed in activity sidebar #60617
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
base: master
Are you sure you want to change the base?
fix(comments): dismiss mention notifications and clear unread badge when viewed in activity sidebar #60617
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,10 +6,14 @@ | |
| import type { INode } from '@nextcloud/files' | ||
| import type { App } from 'vue' | ||
|
|
||
| import { getCurrentUser } from '@nextcloud/auth' | ||
| import axios from '@nextcloud/axios' | ||
| import { generateUrl } from '@nextcloud/router' | ||
| import { createPinia } from 'pinia' | ||
| import { createApp } from 'vue' | ||
| import logger from './logger.ts' | ||
| import { getComments } from './services/GetComments.ts' | ||
| import { markCommentsAsRead } from './services/ReadComments.ts' | ||
|
|
||
| /** | ||
| * Register the comments plugins for the Activity sidebar | ||
|
|
@@ -50,6 +54,26 @@ export function registerCommentsPlugins() { | |
| }, | ||
| ) | ||
| logger.debug('Loaded comments', { node, comments }) | ||
|
|
||
| // Mark all comments as read and clear the unread badge in the files list | ||
| if (node.fileid) { | ||
| markCommentsAsRead('files', node.fileid, new Date()) | ||
| .then(() => node.update({ 'comments-unread': 0 })) | ||
| .catch(() => {}) | ||
| } | ||
|
|
||
| // Mark mention notifications as read for comments that mention the current user | ||
| const currentUser = getCurrentUser() | ||
| if (currentUser) { | ||
| for (const comment of comments) { | ||
| const mentions = Object.values(comment.props?.mentions ?? {}) as { mentionType: string, mentionId: string }[] | ||
| const isMentioned = comment.props?.id && mentions.some((m) => m.mentionType === 'user' && m.mentionId === currentUser.uid) | ||
| if (isMentioned) { | ||
| axios.delete(generateUrl('/apps/comments/notifications/{id}', { id: comment.props.id })) | ||
| .catch(() => {}) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than 100% silently discarding every DAV and notification failure maybe log a debug-level message with the file/comment ID and error so there's still some observability. |
||
| } | ||
| } | ||
| } | ||
| const { default: CommentView } = await import('./views/ActivityCommentEntry.vue') | ||
|
|
||
| return comments.map((comment) => ({ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1131,9 +1131,6 @@ export default { | |
|
|
||
| // ugly hack to make code work - we need the id to be set but at the same time we need to keep values we want to update | ||
| this.share._share.id = share.id | ||
| // Similarly the token is always set by the backend when the | ||
| // share is created. | ||
| this.share._share.token = share.token | ||
|
Comment on lines
-1134
to
-1136
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure what this is for - is this change unrelated to the PR?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I have a suspicion this may be contributing the significant generated |
||
| await this.queueUpdate(...permissionsAndAttributes) | ||
| // Also a ugly hack to update the updated permissions | ||
| for (const prop of permissionsAndAttributes) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||||
| /** | ||||||||||||||
| * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors | ||||||||||||||
| * SPDX-License-Identifier: AGPL-3.0-or-later | ||||||||||||||
| */ | ||||||||||||||
|
|
||||||||||||||
| import type { User } from '@nextcloud/e2e-test-server/cypress' | ||||||||||||||
|
|
||||||||||||||
| import axios from 'axios' | ||||||||||||||
| import { getInlineActionEntryForFile, triggerInlineActionForFile } from '../files/FilesUtils.ts' | ||||||||||||||
|
|
||||||||||||||
| /** | ||||||||||||||
| * Add a comment to a file via the DAV API. | ||||||||||||||
| * | ||||||||||||||
| * @param user The user adding the comment | ||||||||||||||
| * @param fileId The numeric file ID | ||||||||||||||
| * @param message The comment text | ||||||||||||||
| */ | ||||||||||||||
| function addCommentViaApi(user: User, fileId: number, message: string) { | ||||||||||||||
| cy.clearCookies().then(async () => { | ||||||||||||||
| await axios({ | ||||||||||||||
| url: `${Cypress.env('baseUrl')}/remote.php/dav/comments/files/${fileId}`, | ||||||||||||||
| method: 'POST', | ||||||||||||||
| auth: { | ||||||||||||||
| username: user.userId, | ||||||||||||||
| password: user.password, | ||||||||||||||
| }, | ||||||||||||||
| headers: { 'Content-Type': 'application/json' }, | ||||||||||||||
| data: JSON.stringify({ actorType: 'users', verb: 'comment', message }), | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| describe('Comments: unread badge in the files list', { testIsolation: true }, () => { | ||||||||||||||
| let fileOwner: User | ||||||||||||||
| let fileId: number | ||||||||||||||
|
|
||||||||||||||
| beforeEach(() => { | ||||||||||||||
| // The admin user can comment on any file without needing a share | ||||||||||||||
| const admin = { userId: 'admin', password: 'admin' } as User | ||||||||||||||
|
|
||||||||||||||
| cy.createRandomUser().then(($user) => { | ||||||||||||||
| fileOwner = $user | ||||||||||||||
| cy.uploadContent(fileOwner, new Blob(['hello']), 'text/plain', '/commented-file.txt') | ||||||||||||||
| .then((response) => { | ||||||||||||||
| fileId = Number.parseInt(response.headers['oc-fileid'] ?? '0') | ||||||||||||||
| // Add a comment as admin so fileOwner has an unread comment | ||||||||||||||
| addCommentViaApi(admin, fileId, 'Hey, take a look at this!') | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| it('shows the unread comments badge when there are unread comments', () => { | ||||||||||||||
| cy.login(fileOwner) | ||||||||||||||
| cy.visit('/apps/files') | ||||||||||||||
|
|
||||||||||||||
| getInlineActionEntryForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
| .should('be.visible') | ||||||||||||||
| .find('button') | ||||||||||||||
| .should('have.attr', 'aria-label') | ||||||||||||||
| .and('match', /new comment/) | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| it('removes the unread badge after opening the sidebar comments tab', () => { | ||||||||||||||
| cy.login(fileOwner) | ||||||||||||||
| cy.visit('/apps/files') | ||||||||||||||
|
|
||||||||||||||
| // Verify badge is present first | ||||||||||||||
| getInlineActionEntryForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this guaranteed to exercise the Activity path (the server/apps/comments/src/utils/activity.ts Lines 8 to 13 in f5b5225
|
||||||||||||||
| .should('be.visible') | ||||||||||||||
|
|
||||||||||||||
| // Intercept the PROPPATCH that marks comments as read | ||||||||||||||
| cy.intercept('PROPPATCH', /\/remote\.php\/dav\/comments\/files\//).as('markRead') | ||||||||||||||
|
|
||||||||||||||
| // Click the badge — opens the comments or activity sidebar | ||||||||||||||
| triggerInlineActionForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
|
|
||||||||||||||
| cy.get('[data-cy-sidebar]').should('be.visible') | ||||||||||||||
|
|
||||||||||||||
| // The read-marker PROPPATCH must have been sent | ||||||||||||||
| cy.wait('@markRead') | ||||||||||||||
|
|
||||||||||||||
| // Badge must be gone without a page reload | ||||||||||||||
| getInlineActionEntryForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
| .should('not.exist') | ||||||||||||||
| }) | ||||||||||||||
|
|
||||||||||||||
| it('badge stays absent after closing and re-opening the sidebar', () => { | ||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Says "after closing and re-opening the sidebar" but only closes it, no? |
||||||||||||||
| cy.login(fileOwner) | ||||||||||||||
| cy.visit('/apps/files') | ||||||||||||||
|
|
||||||||||||||
| cy.intercept('PROPPATCH', /\/remote\.php\/dav\/comments\/files\//).as('markRead') | ||||||||||||||
|
|
||||||||||||||
| triggerInlineActionForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
| cy.get('[data-cy-sidebar]').should('be.visible') | ||||||||||||||
| cy.wait('@markRead') | ||||||||||||||
|
|
||||||||||||||
| // Close the sidebar | ||||||||||||||
| cy.get('[data-cy-sidebar] .app-sidebar__close').click({ force: true }) | ||||||||||||||
| cy.get('[data-cy-sidebar]').should('not.be.visible') | ||||||||||||||
|
|
||||||||||||||
| // Badge must still be gone | ||||||||||||||
| getInlineActionEntryForFile('commented-file.txt', 'comments-unread') | ||||||||||||||
| .should('not.exist') | ||||||||||||||
| }) | ||||||||||||||
| }) | ||||||||||||||
This file was deleted.
This file was deleted.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 5638-5638.js.license |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| import{a as t}from"./index-C1xmmKTZ-BdqLiU2K.chunk.mjs";import{t as e}from"./translation-DoG5ZELJ-CPJIGC2H.chunk.mjs";import{C as m,a}from"./CommentView-CX3IUGTX.chunk.mjs";import{l as p}from"./activity-CS_yDSTQ.chunk.mjs";import{b as i,r as s,o as n,c,m as u}from"./preload-helper-CX9gtE7n.chunk.mjs";import{_ as l}from"./public-C1mLBHT3.chunk.mjs";import"./index-C6zIcU-d.chunk.mjs";import"./NcModal-kyWZ3UFC-CV6Hvf6d.chunk.mjs";import"./ArrowRight-JDdFcric.chunk.mjs";import"./Web-lLWc6zap.chunk.mjs";import"./index-CziSTDUD.chunk.mjs";import"./TrashCanOutline-B-GxU5E3.chunk.mjs";import"./mdi-BjKyjJ9m.chunk.mjs";import"./pinia-Bx2CoJV6.chunk.mjs";import"./PencilOutline-Bexowggr.chunk.mjs";/* empty css */import"./NcAvatar-ruClKRzS-DbbQH8Qz.chunk.mjs";import"./index-BpWtOFbq.chunk.mjs";import"./util-BUyb4W9M.chunk.mjs";import"./colors-BfjxNgsx-CknPG731.chunk.mjs";import"./NcUserStatusIcon-JWiuiAXe-FPvdKWWr.chunk.mjs";import"./NcDateTime.vue_vue_type_script_setup_true_lang-B4upiZjL-CDStkW4e.chunk.mjs";import"./NcUserBubble-BE6yD-R0-BI6ZwX-N.chunk.mjs";import"./ReadComments-CMOQZLlB.chunk.mjs";import"./index-COu4RIcm.chunk.mjs";const d=i({components:{Comment:a},mixins:[m],props:{reloadCallback:{type:Function,required:!0}},methods:{onNewComment(){try{this.reloadCallback()}catch(o){t(e("comments","Could not reload comments")),p.error("Could not reload comments",{error:o})}}}});function C(o,f,y,w,D,N){const r=s("Comment");return n(),c(r,u(o.editorData,{autoComplete:o.autoComplete,resourceType:o.resourceType,editor:!0,userData:o.userData,resourceId:o.resourceId,class:"comments-action",onNew:o.onNewComment}),null,16,["autoComplete","resourceType","userData","resourceId","onNew"])}const R=l(d,[["render",C],["__scopeId","data-v-29a1e244"]]);export{R as default}; | ||
| //# sourceMappingURL=ActivityCommentAction-CsbmNmci.chunk.mjs.map |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily a blocker for this narrow endpoint, but narrowing the caught exception to comment lookup related failures would avoid masking unrelated errors as missing comments and improve diagnosability.