diff --git a/i18n/en.json b/i18n/en.json index 9edc9a2e..9b57d79d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -348,6 +348,7 @@ "sd-autopreview": "Preview the comment as I type", "sd-collapsethreadslevel": "Autocollapse threads at level", "sd-collapsethreadslevel-help": "0 to never autocollapse.", + "sd-counteditsasnewcomments": "Include edits to existing comments in the number of unseen comments in the navigation panel", "sd-desktopnotifications": "Desktop notifications", "sd-desktopnotifications-radio-all": "Notify me about replies to my comments and comments in topic I'm {{gender:$1|subscribed to}}", "sd-desktopnotifications-radio-tome": "Notify me about replies to my comments only", diff --git a/i18n/qqq.json b/i18n/qqq.json index e06d6149..1ec56f94 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -329,6 +329,7 @@ "sd-autopreview": "Label of the checkbox in the settings dialog.", "sd-collapsethreadslevel": "The number input follows the label (for example, \"Autocollapse threads at level... 10\"). The user is supposed to set the level of comments.\n\n----\nLabel of the number input in the settings dialog.", "sd-collapsethreadslevel-help": "It means \"Set the setting value to 0 to never autocollapse comment threads\".\n\nHelp text for the number input labeled with the {{msg-wm|Convenient-discussions-sd-collapsethreadslevel}} message in the script settings dialog.", + "sd-counteditsasnewcomments": "\"Include\" here is [[wikt:imperfective aspect|imperfective]]: include each time.\n\n----\nLabel of the checkbox in the settings dialog.", "sd-desktopnotifications": "Label of the radio select in the settings dialog.", "sd-desktopnotifications-radio-all": "Label of the item of the radio select labeled with the {{msg-wm|Convenient-discussions-sd-desktopnotifications}} message in the settings dialog.\n\n* $1 (optional): [[mw:Manual:Messages API#GENDER in JavaScript|the user object]], for use with {{gender:}} ({{gender:$2|''male text''|''female text''|''text for unspecified''}}).", "sd-desktopnotifications-radio-tome": "Label of the item of the radio select labeled with the {{msg-wm|Convenient-discussions-sd-desktopnotifications}} message in the settings dialog.", diff --git a/i18n/ru.json b/i18n/ru.json index 92b958c6..5d1e7f05 100644 --- a/i18n/ru.json +++ b/i18n/ru.json @@ -331,6 +331,7 @@ "sd-autopreview": "Предпросматривать сообщение по мере набора", "sd-collapsethreadslevel": "Автоматически сворачивать ветки, начиная с уровня", "sd-collapsethreadslevel-help": "Установите значение 0, чтобы не сворачивать ветки автоматически.", + "sd-counteditsasnewcomments": "Включать правки существующих сообщений в число непросмотренных сообщений в навигационной панели", "sd-desktopnotifications": "Уведомления на рабочий стол", "sd-desktopnotifications-radio-all": "Об ответах на мои сообщения и о сообщениях в разделах, на которые я {{gender:$1|подписан|подписана}}", "sd-desktopnotifications-radio-tome": "Только об ответах на мои сообщения", diff --git a/src/Comment.js b/src/Comment.js index 619052f9..729512c1 100644 --- a/src/Comment.js +++ b/src/Comment.js @@ -44,6 +44,7 @@ class Comment extends CommentSkeleton { this.hideTimezone = settings.get('hideTimezone'); this.timestampFormat = settings.get('timestampFormat'); this.useUiTime = settings.get('useUiTime'); + this.countEditsAsNewComments = settings.get('countEditsAsNewComments'); /** * Comment author user object. @@ -2006,34 +2007,17 @@ class Comment extends CommentSkeleton { * @private */ async showDiff(comparedRevisionId, commentsData) { - let revisionIdLesser = Math.min(mw.config.get('wgRevisionId'), comparedRevisionId); - let revisionIdGreater = Math.max(mw.config.get('wgRevisionId'), comparedRevisionId); - - const revisionsRequest = controller.getApi().post({ - action: 'query', - revids: [revisionIdLesser, revisionIdGreater], - prop: 'revisions', - rvslots: 'main', - rvprop: ['ids', 'content'], - redirects: !mw.config.get('wgIsRedirect'), - }).catch(handleApiReject); - - const compareRequest = controller.getApi().post({ - action: 'compare', - fromtitle: this.getSourcePage().name, - fromrev: revisionIdLesser, - torev: revisionIdGreater, - prop: ['diff'], - }).catch(handleApiReject); - - let [revisionsResp, compareResp] = await Promise.all([ - revisionsRequest, - compareRequest, + const revisionIdLesser = Math.min(mw.config.get('wgRevisionId'), comparedRevisionId); + const revisionIdGreater = Math.max(mw.config.get('wgRevisionId'), comparedRevisionId); + + let [revisions, body] = await Promise.all([ + this.getSourcePage().getRevisions({ + revids: [revisionIdLesser, revisionIdGreater], + rvprop: ['content'], + }), + this.getSourcePage().compareRevisions(revisionIdLesser, revisionIdGreater), mw.loader.using(['mediawiki.diff', 'mediawiki.diff.styles']), ]); - - const revisions = revisionsResp.query?.pages?.[0]?.revisions; - const body = compareResp?.compare?.body; if (!revisions || body === undefined) { throw new CdError({ type: 'api', @@ -2057,29 +2041,31 @@ class Comment extends CommentSkeleton { .addClass('cd-commentDiffView-below') .append( $('') - .attr('href', this.getSourcePage().getUrl({ + .attr('href', cd.page.getUrl({ oldid: revisionIdLesser, diff: revisionIdGreater, })) .attr('target', '_blank') - // Make it work in https://ru.wikipedia.org/wiki/User:Serhio_Magpie/instantDiffs.js + // Make it work in https://commons.wikimedia.org/wiki/User:Serhio_Magpie/instantDiffs.js .attr('data-instantdiffs-link', 'link') .text(cd.s('comment-diff-full')), cd.sParse('dot-separator'), $('') - .attr('href', this.getSourcePage().getUrl({ action: 'history' })) + .attr('href', cd.page.getUrl({ action: 'history' })) .attr('target', '_blank') .text(cd.s('comment-diff-history')) ) ) .children(); - mw.hook('wikipage.content').fire($message); OO.ui.alert($message, { title: cd.s('comment-diff-title'), size: 'larger', }); + + // FIXME: "wikipage.content hook should not be fired on unattached content". + mw.hook('wikipage.content').fire($message); } /** @@ -2087,13 +2073,21 @@ class Comment extends CommentSkeleton { * been changed or deleted, and change the comment's styling if it has been. * * @param {'changed'|'changedSince'|'deleted'} type Type of the mark. - * @param {boolean} [isNewVersionRendered] Has the new version of the comment been rendered. + * @param {boolean} [isNewVersionRendered] Is the new version of the comment rendered + * (successfully updated or, for `changedSince` type, has been a new one from the beginning). * @param {number} [comparedRevisionId] ID of the revision to compare with when the user clicks to * see the diff. * @param {object} [commentsData] Data of the comments as of the current revision and the revision * to compare with. - */ - markAsChanged(type, isNewVersionRendered, comparedRevisionId, commentsData) { + * @param {boolean} [showDiffLink=true] Whether to show the diff link if it makes sense. + */ + async markAsChanged( + type, + isNewVersionRendered, + comparedRevisionId, + commentsData, + showDiffLink = true + ) { let stringName; switch (type) { case 'changed': @@ -2122,8 +2116,7 @@ class Comment extends CommentSkeleton { }, }); - const diffLink = type === 'deleted' || this.getSourcePage() !== cd.page ? - undefined : + const diffLink = showDiffLink && this.getSourcePage().isCurrent() && type !== 'deleted' ? new Button({ label: cd.s('comment-diff'), action: async () => { @@ -2140,11 +2133,12 @@ class Comment extends CommentSkeleton { text += ' ' + cd.sParse('error-network'); } } - mw.notify(wrapHtml(text), { type: e.data?.code === 'emptyDiff'? 'info' : 'error' }); + mw.notify(wrapHtml(text), { type: e.data?.code === 'emptyDiff' ? 'info' : 'error' }); } diffLink.setPending(false); }, - }); + }) : + undefined; let refreshLinkSeparator; let diffLinkSeparator; @@ -2194,6 +2188,10 @@ class Comment extends CommentSkeleton { this.flashChangedOnSight(); } + if (this.countEditsAsNewComments && (type === 'changed' || type === 'changedSince')) { + this.isSeen = false; + } + // Layers are supposed to be updated (deleted comments background, repositioning) separately, // see updateChecker~checkForNewChanges(), for example. } diff --git a/src/pageRegistry.js b/src/pageRegistry.js index cf2b052c..4234fb1e 100644 --- a/src/pageRegistry.js +++ b/src/pageRegistry.js @@ -525,7 +525,7 @@ export class Page { async getRevisions(customOptions = {}, inBackground = false) { const options = Object.assign({}, { action: 'query', - titles: this.name, + titles: customOptions.revids ? undefined : this.name, rvslots: 'main', prop: 'revisions', redirects: !(this.isCurrent() && mw.config.get('wgIsRedirect')), @@ -927,6 +927,23 @@ export class Page { return this; } + /** + * Get a diff between two revisions of the page. + * + * @param {number} revisionIdFrom + * @param {number} revisionIdTo + * @returns {Promise.} + */ + async compareRevisions(revisionIdFrom, revisionIdTo) { + return (await controller.getApi().post({ + action: 'compare', + fromtitle: this.name, + fromrev: revisionIdFrom, + torev: revisionIdTo, + prop: ['diff'], + }).catch(handleApiReject))?.compare?.body; + } + /** * Set some map object variables related to archive pages. * diff --git a/src/settings.js b/src/settings.js index c509eacb..d78b6e54 100644 --- a/src/settings.js +++ b/src/settings.js @@ -78,6 +78,7 @@ export default { 'autopreview': true, 'collapseThreadsLevel': 10, + 'countEditsAsNewComments': false, 'desktopNotifications': 'unknown', 'enableThreads': true, 'hideTimezone': false, @@ -182,11 +183,16 @@ export default { name: 'highlightNewInterval', type: 'number', min: 0, - max: 99999999, + max: 9999999, buttonStep: 5, label: cd.s('sd-highlightnewinterval'), help: cd.s('sd-highlightnewinterval-help'), }, + { + name: 'countEditsAsNewComments', + type: 'checkbox', + label: cd.s('sd-counteditsasnewcomments'), + }, { name: 'improvePerformance', type: 'checkbox', diff --git a/src/updateChecker.js b/src/updateChecker.js index 732f06da..0f4f4c60 100644 --- a/src/updateChecker.js +++ b/src/updateChecker.js @@ -345,7 +345,7 @@ async function checkForUpdates() { mapSections(sections); mapComments(currentComments, newComments); - updateChecker.emit('sectionsUpdate', sections) + updateChecker.emit('sectionsUpdate', sections); // We check for changes before notifying about new comments to notify about changes in a // renamed section if it is watched. @@ -406,6 +406,7 @@ function checkForChangesSincePreviousVisit(currentComments, submittedCommentId) const seen = seenStorageItem.get(mw.config.get('wgArticleId')); const changeList = []; + const markAsChangedData = []; currentComments.forEach((currentComment) => { if (currentComment.id === submittedCommentId) return; @@ -427,7 +428,12 @@ function checkForChangesSincePreviousVisit(currentComments, submittedCommentId) 1: currentComment, }; - comment.markAsChanged('changedSince', true, previousVisitRevisionId, commentsData); + markAsChangedData.push({ + comment, + isNewRevisionRendered: true, + comparedRevisionId: previousVisitRevisionId, + commentsData, + }); if (comment.isOpeningSection) { comment.section?.resubscribeIfRenamed(currentComment, oldComment); @@ -438,6 +444,13 @@ function checkForChangesSincePreviousVisit(currentComments, submittedCommentId) } }); + markCommentsAsChanged( + 'changedSince', + markAsChangedData, + previousVisitRevisionId, + mw.config.get('wgRevisionId') + ); + if (changeList.length) { /** * Existing comments have changed since the previous visit. @@ -464,12 +477,13 @@ function checkForChangesSincePreviousVisit(currentComments, submittedCommentId) */ function checkForNewChanges(currentComments) { const changeList = []; + const markAsChangedData = []; currentComments.forEach((currentComment) => { const newComment = currentComment.match; let comment; const events = {}; - // Different indexes to supply one object both to the event and Comment#markAsChanged. + // Different indexes to supply one object both to the event and Comment#markAsChanged(). const commentsData = { current: currentComment, new: newComment, @@ -494,7 +508,12 @@ function checkForNewChanges(currentComments) { // Comment#flashChanged() called indirectly by Comment#markAsChanged(). comment.htmlToCompare = newComment.htmlToCompare; - comment.markAsChanged('changed', updateSuccess, lastCheckedRevisionId, commentsData); + markAsChangedData.push({ + comment, + isNewRevisionRendered: updateSuccess, + comparedRevisionId: lastCheckedRevisionId, + commentsData, + }); events.changed = { updateSuccess }; } } else if (comment.isChanged) { @@ -515,6 +534,13 @@ function checkForNewChanges(currentComments) { } }); + markCommentsAsChanged( + 'changed', + markAsChangedData, + mw.config.get('wgRevisionId'), + lastCheckedRevisionId + ); + if (changeList.length) { updateChecker.emit('newChanges', changeList); @@ -537,6 +563,69 @@ function checkForNewChanges(currentComments) { } } +/** + * Data needed to mark the comment as changed + * + * @typedef {object} MarkAsChangedData + * @property {import('./Comment').default} comment + * @property {boolean} updateSuccess + * @property {object} commentsData + * @private + */ + +/** + * Mark comments as changed, verifying diffs if possible to decide whether to show the diff link. + * + * @param {'changed'|'changedSince'|'deleted'} type + * @param {MarkAsChangedData[]} data + * @param {number} revisionIdLesser + * @param {number} revisionIdGreater + */ +async function markCommentsAsChanged(type, data, revisionIdLesser, revisionIdGreater) { + if (!data.length) return; + + const currentRevisionId = mw.config.get('wgRevisionId'); + + // Don't process >20 diffs, that's too much and probably means something is broken + const verifyDiffs = ( + data.length <= 20 && + data.some(({ comment }) => comment.getSourcePage().isCurrent()) + ); + + let revisions; + let compareBody; + if (verifyDiffs) { + try { + revisions = await cd.page.getRevisions({ + revids: [revisionIdLesser, revisionIdGreater], + rvprop: ['content'], + }); + compareBody = await cd.page.compareRevisions(revisionIdLesser, revisionIdGreater); + } catch { + // Empty + } + } + if (!isPageStillAtRevision(currentRevisionId)) return; + + data.forEach(({ comment, isNewRevisionRendered, comparedRevisionId, commentsData }) => { + comment.markAsChanged( + type, + isNewRevisionRendered, + comparedRevisionId, + commentsData, + verifyDiffs && compareBody !== undefined && revisions !== undefined ? + Boolean( + comment.scrubDiff(compareBody, revisions, commentsData) + .find('.diff-deletedline, .diff-addedline') + .length + ) : + true + ); + }); + + commentRegistry.emit('registerSeen'); +} + /** * Check if the page is still at the specified revision and nothing is loading. *