2019-11-05 13:55:01 +00:00
|
|
|
'use strict';
|
|
|
|
|
2021-08-19 20:35:32 +00:00
|
|
|
/* global moment */
|
2019-12-10 21:46:22 +00:00
|
|
|
var
|
2021-08-23 20:23:37 +00:00
|
|
|
$pageContainer, linksController, lastHighlightComment,
|
2021-01-25 14:15:35 +00:00
|
|
|
featuresEnabled = mw.config.get( 'wgDiscussionToolsFeaturesEnabled' ) || {},
|
2021-08-23 20:23:37 +00:00
|
|
|
seenAutoTopicSubPopup = !!+mw.user.options.get( 'discussiontools-seenautotopicsubpopup' ),
|
2021-06-24 16:21:31 +00:00
|
|
|
storage = mw.storage.session,
|
2020-10-21 15:52:04 +00:00
|
|
|
Parser = require( './Parser.js' ),
|
2020-09-16 12:07:27 +00:00
|
|
|
ThreadItem = require( './ThreadItem.js' ),
|
2021-08-19 20:35:32 +00:00
|
|
|
HeadingItem = require( './HeadingItem.js' ),
|
|
|
|
CommentItem = require( './CommentItem.js' ),
|
2021-06-09 23:22:28 +00:00
|
|
|
CommentDetails = require( './CommentDetails.js' ),
|
2021-06-24 16:21:31 +00:00
|
|
|
ReplyLinksController = require( './ReplyLinksController.js' ),
|
2020-08-19 20:03:41 +00:00
|
|
|
logger = require( './logger.js' ),
|
2021-02-17 22:34:02 +00:00
|
|
|
utils = require( './utils.js' ),
|
2021-08-19 20:35:32 +00:00
|
|
|
STATE_UNSUBSCRIBED = 0,
|
|
|
|
STATE_SUBSCRIBED = 1,
|
2021-08-23 20:23:37 +00:00
|
|
|
STATE_AUTOSUBSCRIBED = 2,
|
2020-04-30 13:20:41 +00:00
|
|
|
pageDataCache = {};
|
2019-11-05 13:55:01 +00:00
|
|
|
|
2020-02-25 02:10:27 +00:00
|
|
|
mw.messages.set( require( './controller/contLangMessages.json' ) );
|
2020-03-10 13:03:45 +00:00
|
|
|
|
2020-10-23 11:02:18 +00:00
|
|
|
function getApi() {
|
2020-10-23 11:32:00 +00:00
|
|
|
return new mw.Api( {
|
|
|
|
parameters: {
|
|
|
|
formatversion: 2,
|
|
|
|
uselang: mw.config.get( 'wgUserLanguage' )
|
|
|
|
}
|
|
|
|
} );
|
2020-10-23 11:02:18 +00:00
|
|
|
}
|
|
|
|
|
2021-04-29 18:03:37 +00:00
|
|
|
/**
|
|
|
|
* Draw a semi-transparent rectangle on the page to highlight the given comment.
|
|
|
|
*
|
|
|
|
* @param {CommentItem} comment
|
|
|
|
* @return {jQuery} Highlight node
|
|
|
|
*/
|
2019-11-13 13:57:57 +00:00
|
|
|
function highlight( comment ) {
|
|
|
|
var padding = 5,
|
2021-03-13 14:39:39 +00:00
|
|
|
$highlight = $( '<div>' ).addClass( 'ext-discussiontools-init-highlight' );
|
2019-11-13 13:57:57 +00:00
|
|
|
|
2021-05-31 19:31:01 +00:00
|
|
|
// We insert the highlight in the DOM near the comment, so that it remains positioned correctly
|
|
|
|
// when it shifts (e.g. collapsing the table of contents), and disappears when it is hidden (e.g.
|
|
|
|
// opening visual editor).
|
|
|
|
var range = comment.getNativeRange();
|
|
|
|
// Support: Firefox, IE 11
|
|
|
|
// The highlight node must be inserted after the start marker node (data-mw-comment-start), not
|
|
|
|
// before, otherwise Node#getBoundingClientRect() returns wrong results.
|
|
|
|
range.insertNode( $highlight[ 0 ] );
|
|
|
|
|
|
|
|
var baseRect = $highlight[ 0 ].getBoundingClientRect();
|
|
|
|
var rect = RangeFix.getBoundingClientRect( range );
|
2021-07-12 21:15:47 +00:00
|
|
|
// rect may be null if the range is in a detached or hidden node
|
|
|
|
if ( rect ) {
|
|
|
|
$highlight.css( {
|
|
|
|
'margin-top': rect.top - baseRect.top - padding,
|
|
|
|
'margin-left': rect.left - baseRect.left - padding,
|
|
|
|
width: rect.width + ( padding * 2 ),
|
|
|
|
height: rect.height + ( padding * 2 )
|
|
|
|
} );
|
|
|
|
}
|
2019-11-13 13:57:57 +00:00
|
|
|
|
2021-04-29 18:03:37 +00:00
|
|
|
return $highlight;
|
2019-11-13 13:57:57 +00:00
|
|
|
}
|
|
|
|
|
2020-02-15 04:03:18 +00:00
|
|
|
/**
|
2020-08-19 20:03:41 +00:00
|
|
|
* Get various pieces of page metadata.
|
2020-02-15 04:03:18 +00:00
|
|
|
*
|
|
|
|
* This method caches responses. If you call it again with the same parameters, you'll get the exact
|
|
|
|
* same Promise object, and no API request will be made.
|
|
|
|
*
|
|
|
|
* @param {string} pageName Page title
|
|
|
|
* @param {number} oldId Revision ID
|
2021-03-24 18:41:39 +00:00
|
|
|
* @param {boolean} [isNewTopic=false]
|
2020-02-15 04:03:18 +00:00
|
|
|
* @return {jQuery.Promise}
|
|
|
|
*/
|
2021-03-24 18:41:39 +00:00
|
|
|
function getPageData( pageName, oldId, isNewTopic ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var api = getApi();
|
2020-10-23 11:02:18 +00:00
|
|
|
|
2020-02-15 04:03:18 +00:00
|
|
|
pageDataCache[ pageName ] = pageDataCache[ pageName ] || {};
|
|
|
|
if ( pageDataCache[ pageName ][ oldId ] ) {
|
|
|
|
return pageDataCache[ pageName ][ oldId ];
|
|
|
|
}
|
2020-06-02 23:00:59 +00:00
|
|
|
|
2021-04-08 13:46:09 +00:00
|
|
|
var lintPromise, transcludedFromPromise;
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( oldId && !isNewTopic ) {
|
2020-09-16 11:19:42 +00:00
|
|
|
lintPromise = api.get( {
|
|
|
|
action: 'query',
|
|
|
|
list: 'linterrors',
|
|
|
|
lntcategories: 'fostered',
|
|
|
|
lntlimit: 1,
|
|
|
|
lnttitle: pageName
|
|
|
|
} ).then( function ( response ) {
|
|
|
|
return OO.getProp( response, 'query', 'linterrors' ) || [];
|
|
|
|
} );
|
2020-06-02 23:00:59 +00:00
|
|
|
|
2020-09-16 11:19:42 +00:00
|
|
|
transcludedFromPromise = api.get( {
|
|
|
|
action: 'discussiontools',
|
|
|
|
paction: 'transcludedfrom',
|
|
|
|
page: pageName,
|
|
|
|
oldid: oldId
|
|
|
|
} ).then( function ( response ) {
|
2020-09-16 20:30:11 +00:00
|
|
|
return OO.getProp( response, 'discussiontools' ) || {};
|
2020-09-16 11:19:42 +00:00
|
|
|
} );
|
|
|
|
} else {
|
2020-09-16 20:30:11 +00:00
|
|
|
lintPromise = $.Deferred().resolve( [] ).promise();
|
|
|
|
transcludedFromPromise = $.Deferred().resolve( {} ).promise();
|
2020-09-16 11:19:42 +00:00
|
|
|
}
|
2020-08-19 20:03:41 +00:00
|
|
|
|
2021-04-08 13:46:09 +00:00
|
|
|
var veMetadataPromise = api.get( {
|
2020-08-19 20:03:41 +00:00
|
|
|
action: 'visualeditor',
|
|
|
|
paction: 'metadata',
|
|
|
|
page: pageName
|
|
|
|
} ).then( function ( response ) {
|
|
|
|
return OO.getProp( response, 'visualeditor' ) || [];
|
|
|
|
} );
|
|
|
|
|
|
|
|
pageDataCache[ pageName ][ oldId ] = $.when( lintPromise, transcludedFromPromise, veMetadataPromise )
|
|
|
|
.then( function ( linterrors, transcludedfrom, metadata ) {
|
|
|
|
return {
|
|
|
|
linterrors: linterrors,
|
|
|
|
transcludedfrom: transcludedfrom,
|
|
|
|
metadata: metadata
|
|
|
|
};
|
|
|
|
}, function () {
|
|
|
|
// Clear on failure
|
|
|
|
pageDataCache[ pageName ][ oldId ] = null;
|
2020-08-12 21:37:10 +00:00
|
|
|
// Let caller handle the error
|
|
|
|
return $.Deferred().rejectWith( this, arguments );
|
2020-08-19 20:03:41 +00:00
|
|
|
} );
|
2020-02-15 04:03:18 +00:00
|
|
|
return pageDataCache[ pageName ][ oldId ];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-08-19 20:03:41 +00:00
|
|
|
* Check if a given comment on a page can be replied to
|
2020-02-15 04:03:18 +00:00
|
|
|
*
|
2020-03-12 19:58:36 +00:00
|
|
|
* @param {string} pageName Page title
|
|
|
|
* @param {number} oldId Revision ID
|
2021-04-07 20:46:36 +00:00
|
|
|
* @param {CommentItem} comment Comment
|
2021-06-09 23:22:28 +00:00
|
|
|
* @return {jQuery.Promise} Resolved with a CommentDetails object if the comment appears on the page.
|
2020-08-19 20:03:41 +00:00
|
|
|
* Rejects with error data if the comment is transcluded, or there are lint errors on the page.
|
2020-02-15 04:03:18 +00:00
|
|
|
*/
|
2021-04-07 20:46:36 +00:00
|
|
|
function checkCommentOnPage( pageName, oldId, comment ) {
|
2021-06-24 16:21:31 +00:00
|
|
|
var isNewTopic = comment.id === utils.NEW_TOPIC_COMMENT_ID;
|
2021-03-24 18:41:39 +00:00
|
|
|
|
|
|
|
return getPageData( pageName, oldId, isNewTopic )
|
2020-02-15 04:03:18 +00:00
|
|
|
.then( function ( response ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var metadata = response.metadata,
|
2020-08-19 20:03:41 +00:00
|
|
|
lintErrors = response.linterrors,
|
|
|
|
transcludedFrom = response.transcludedfrom;
|
|
|
|
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( !isNewTopic ) {
|
2021-04-07 20:46:36 +00:00
|
|
|
// First look for data by the comment's ID. If not found, also look by name.
|
|
|
|
// Data by ID may not be found due to differences in headings (e.g. T273413, T275821),
|
|
|
|
// or if a comment's parent changes.
|
|
|
|
// Data by name might be combined from two or more comments, which would only allow us to
|
|
|
|
// treat them both as transcluded from unknown source, unless we check ID first.
|
2021-04-08 13:46:09 +00:00
|
|
|
var isTranscludedFrom = transcludedFrom[ comment.id ] || transcludedFrom[ comment.name ];
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( isTranscludedFrom === undefined ) {
|
|
|
|
// The comment wasn't found when generating the "transcludedfrom" data,
|
|
|
|
// so we don't know where the reply should be posted. Just give up.
|
|
|
|
return $.Deferred().reject( 'discussiontools-commentid-notfound-transcludedfrom', { errors: [ {
|
|
|
|
code: 'discussiontools-commentid-notfound-transcludedfrom',
|
|
|
|
html: mw.message( 'discussiontools-error-comment-disappeared' ).parse()
|
|
|
|
} ] } ).promise();
|
|
|
|
} else if ( isTranscludedFrom ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var mwTitle = isTranscludedFrom === true ? null : mw.Title.newFromText( isTranscludedFrom );
|
2021-03-24 18:41:39 +00:00
|
|
|
// If this refers to a template rather than a subpage, we never want to edit it
|
2021-04-08 13:46:09 +00:00
|
|
|
var follow = mwTitle && mwTitle.getNamespaceId() !== mw.config.get( 'wgNamespaceIds' ).template;
|
2021-03-24 18:41:39 +00:00
|
|
|
|
2021-04-08 13:46:09 +00:00
|
|
|
var transcludedErrMsg;
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( follow ) {
|
|
|
|
transcludedErrMsg = mw.message(
|
|
|
|
'discussiontools-error-comment-is-transcluded-title',
|
|
|
|
mwTitle.getPrefixedText()
|
|
|
|
).parse();
|
|
|
|
} else {
|
|
|
|
transcludedErrMsg = mw.message(
|
|
|
|
'discussiontools-error-comment-is-transcluded',
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
$( '#ca-edit' ).text()
|
|
|
|
).parse();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $.Deferred().reject( 'comment-is-transcluded', { errors: [ {
|
|
|
|
data: {
|
|
|
|
transcludedFrom: isTranscludedFrom,
|
|
|
|
follow: follow
|
|
|
|
},
|
|
|
|
code: 'comment-is-transcluded',
|
|
|
|
html: transcludedErrMsg
|
|
|
|
} ] } ).promise();
|
2020-03-09 20:53:41 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( lintErrors.length ) {
|
|
|
|
// We currently only request the first error
|
2021-04-08 13:46:09 +00:00
|
|
|
var lintType = lintErrors[ 0 ].category;
|
2021-03-24 18:41:39 +00:00
|
|
|
|
|
|
|
return $.Deferred().reject( 'lint', { errors: [ {
|
|
|
|
code: 'lint',
|
|
|
|
html: mw.message( 'discussiontools-error-lint',
|
|
|
|
'https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Lint_errors/' + lintType,
|
|
|
|
'https://www.mediawiki.org/wiki/Special:MyLanguage/Help_talk:Lint_errors/' + lintType,
|
|
|
|
mw.util.getUrl( pageName, { action: 'edit', lintid: lintErrors[ 0 ].lintId } ) ).parse()
|
|
|
|
} ] } ).promise();
|
|
|
|
}
|
2020-03-09 20:53:41 +00:00
|
|
|
}
|
|
|
|
|
2021-03-24 18:41:39 +00:00
|
|
|
if ( !metadata.canEdit ) {
|
|
|
|
return $.Deferred().reject( 'permissions-error', { errors: [ {
|
|
|
|
code: 'permissions-error',
|
|
|
|
html: metadata.notices[ 'permissions-error' ]
|
2020-06-03 22:19:44 +00:00
|
|
|
} ] } ).promise();
|
2020-04-15 19:04:09 +00:00
|
|
|
}
|
|
|
|
|
2020-12-03 22:32:35 +00:00
|
|
|
return new CommentDetails( pageName, oldId, metadata.notices );
|
2020-02-15 04:03:18 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
function getCheckboxesPromise( pageName, oldId ) {
|
2020-03-04 16:19:19 +00:00
|
|
|
return getPageData(
|
2020-08-19 20:03:41 +00:00
|
|
|
pageName,
|
|
|
|
oldId
|
|
|
|
).then( function ( pageData ) {
|
|
|
|
var data = pageData.metadata,
|
2020-03-04 16:19:19 +00:00
|
|
|
checkboxesDef = {};
|
|
|
|
|
|
|
|
mw.messages.set( data.checkboxesMessages );
|
|
|
|
|
|
|
|
// Only show the watch checkbox for now
|
|
|
|
if ( 'wpWatchthis' in data.checkboxesDef ) {
|
|
|
|
checkboxesDef.wpWatchthis = data.checkboxesDef.wpWatchthis;
|
2021-09-21 09:41:32 +00:00
|
|
|
// Override the label with a more verbose one to distinguish this from topic subscriptions (T290712)
|
|
|
|
checkboxesDef.wpWatchthis[ 'label-message' ] = 'discussiontools-replywidget-watchthis';
|
2020-03-04 16:19:19 +00:00
|
|
|
}
|
2020-12-15 19:34:57 +00:00
|
|
|
return mw.loader.using( 'ext.visualEditor.targetLoader' ).then( function () {
|
|
|
|
return mw.libs.ve.targetLoader.createCheckboxFields( checkboxesDef );
|
|
|
|
} );
|
2020-03-04 16:19:19 +00:00
|
|
|
// TODO: createCheckboxField doesn't make links in the label open in a new
|
|
|
|
// window as that method currently lives in ve.utils
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2021-08-19 20:35:32 +00:00
|
|
|
function updateSubscribeButton( element, state ) {
|
|
|
|
if ( state !== null ) {
|
|
|
|
element.setAttribute( 'data-mw-subscribed', String( state ) );
|
|
|
|
}
|
|
|
|
if ( state ) {
|
|
|
|
element.textContent = mw.msg( 'discussiontools-topicsubscription-button-unsubscribe' );
|
|
|
|
element.setAttribute( 'title', mw.msg( 'discussiontools-topicsubscription-button-unsubscribe-tooltip' ) );
|
|
|
|
} else {
|
|
|
|
element.textContent = mw.msg( 'discussiontools-topicsubscription-button-subscribe' );
|
|
|
|
element.setAttribute( 'title', mw.msg( 'discussiontools-topicsubscription-button-subscribe-tooltip' ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-17 22:34:02 +00:00
|
|
|
function initTopicSubscriptions( $container ) {
|
2021-05-05 11:44:51 +00:00
|
|
|
$container.find( '.ext-discussiontools-init-section-subscribe-link' ).on( 'click keypress', function ( e ) {
|
|
|
|
if ( e.type === 'keypress' && e.which !== OO.ui.Keys.ENTER && e.which !== OO.ui.Keys.SPACE ) {
|
|
|
|
// Only handle keypresses on the "Enter" or "Space" keys
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ( e.type === 'click' && ( e.which !== OO.ui.MouseButtons.LEFT || e.shiftKey || e.altKey || e.ctrlKey || e.metaKey ) ) {
|
|
|
|
// Only handle unmodified left clicks
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
var commentName = this.getAttribute( 'data-mw-comment-name' );
|
2021-02-17 22:34:02 +00:00
|
|
|
|
|
|
|
if ( !commentName ) {
|
|
|
|
// This should never happen
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-05 11:44:51 +00:00
|
|
|
var element = this,
|
|
|
|
api = getApi(),
|
2021-08-19 20:35:32 +00:00
|
|
|
subscribedState = element.hasAttribute( 'data-mw-subscribed' ) ?
|
|
|
|
Number( element.getAttribute( 'data-mw-subscribed' ) ) : null,
|
2021-05-05 11:44:51 +00:00
|
|
|
heading = $( this ).closest( '.ext-discussiontools-init-section' )[ 0 ],
|
|
|
|
section = utils.getHeadlineNodeAndOffset( heading ).node.id,
|
|
|
|
title = mw.config.get( 'wgRelevantPageName' ) + '#' + section;
|
|
|
|
|
2021-08-19 20:35:32 +00:00
|
|
|
$( element ).addClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
|
|
|
|
2021-02-17 22:34:02 +00:00
|
|
|
api.postWithToken( 'csrf', {
|
|
|
|
action: 'discussiontoolssubscribe',
|
|
|
|
page: title,
|
|
|
|
commentname: commentName,
|
2021-08-19 20:35:32 +00:00
|
|
|
subscribe: !subscribedState
|
2021-08-16 19:33:59 +00:00
|
|
|
} ).then( function ( response ) {
|
|
|
|
return OO.getProp( response, 'discussiontoolssubscribe' ) || {};
|
2021-02-17 22:34:02 +00:00
|
|
|
} ).then( function ( result ) {
|
2021-08-19 20:35:32 +00:00
|
|
|
$( element ).removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
|
|
|
updateSubscribeButton( element, result.subscribe ? STATE_SUBSCRIBED : STATE_UNSUBSCRIBED );
|
2021-02-17 22:34:02 +00:00
|
|
|
mw.notify(
|
|
|
|
mw.msg(
|
|
|
|
result.subscribe ?
|
|
|
|
'discussiontools-topicsubscription-notify-subscribed-body' :
|
|
|
|
'discussiontools-topicsubscription-notify-unsubscribed-body'
|
|
|
|
),
|
|
|
|
{
|
|
|
|
title: mw.msg(
|
|
|
|
result.subscribe ?
|
|
|
|
'discussiontools-topicsubscription-notify-subscribed-title' :
|
|
|
|
'discussiontools-topicsubscription-notify-unsubscribed-title'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
);
|
2021-04-27 20:21:50 +00:00
|
|
|
}, function ( code, data ) {
|
|
|
|
mw.notify( api.getErrorMessage( data ), { type: 'error' } );
|
2021-08-19 20:35:32 +00:00
|
|
|
$( element ).removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
2021-02-17 22:34:02 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2021-08-23 20:23:37 +00:00
|
|
|
function maybeShowFirstTimeAutoTopicSubPopup() {
|
|
|
|
if ( seenAutoTopicSubPopup ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
seenAutoTopicSubPopup = true;
|
|
|
|
mw.user.options.set( 'discussiontools-seenautotopicsubpopup', '1' );
|
|
|
|
getApi().saveOption( 'discussiontools-seenautotopicsubpopup', '1' );
|
|
|
|
|
|
|
|
var $popupContent, popup;
|
|
|
|
|
|
|
|
if ( !lastHighlightComment ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
popup.$element.removeClass( 'ext-discussiontools-autotopicsubpopup-fadein' );
|
|
|
|
setTimeout( function () {
|
|
|
|
popup.$element.detach();
|
|
|
|
}, 1000 );
|
|
|
|
}
|
|
|
|
|
|
|
|
$popupContent = $( '<div>' )
|
|
|
|
.append(
|
|
|
|
$( '<strong>' )
|
|
|
|
.addClass( 'ext-discussiontools-autotopicsubpopup-title' )
|
|
|
|
.text( mw.msg( 'discussiontools-autotopicsubpopup-title' ) ),
|
|
|
|
$( '<div>' )
|
|
|
|
.addClass( 'ext-discussiontools-autotopicsubpopup-image' ),
|
|
|
|
$( '<div>' )
|
|
|
|
.addClass( 'ext-discussiontools-autotopicsubpopup-body' )
|
|
|
|
.text( mw.msg( 'discussiontools-autotopicsubpopup-body' ) ),
|
|
|
|
$( '<div>' )
|
|
|
|
.addClass( 'ext-discussiontools-autotopicsubpopup-actions' )
|
|
|
|
.append( new OO.ui.ButtonWidget( {
|
|
|
|
label: mw.msg( 'discussiontools-autotopicsubpopup-dismiss' ),
|
|
|
|
flags: [ 'primary', 'progressive' ]
|
|
|
|
} ).on( 'click', close ).$element )
|
|
|
|
.append( new OO.ui.ButtonWidget( {
|
|
|
|
label: mw.msg( 'discussiontools-autotopicsubpopup-preferences' ),
|
|
|
|
href: mw.util.getUrl( 'Special:Preferences#mw-prefsection-editing-discussion' ),
|
|
|
|
framed: false
|
|
|
|
} ).$element )
|
|
|
|
);
|
|
|
|
|
|
|
|
popup = new OO.ui.PopupWidget( {
|
|
|
|
// Styles and dimensions
|
|
|
|
width: '',
|
|
|
|
height: '',
|
|
|
|
anchor: false,
|
|
|
|
autoClose: false,
|
|
|
|
head: false,
|
|
|
|
padded: false,
|
|
|
|
classes: [ 'ext-discussiontools-autotopicsubpopup' ],
|
|
|
|
hideWhenOutOfView: false,
|
|
|
|
// Content
|
|
|
|
$content: $popupContent.contents()
|
|
|
|
} );
|
|
|
|
|
|
|
|
// Like in highlight()
|
|
|
|
lastHighlightComment.getNativeRange().insertNode( popup.$element[ 0 ] );
|
|
|
|
// Pull it outside of headings to avoid silly fonts
|
|
|
|
if ( popup.$element.closest( 'h1, h2, h3, h4, h5, h6' ).length ) {
|
|
|
|
popup.$element.closest( 'h1, h2, h3, h4, h5, h6' ).after( popup.$element );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Disable positioning, the popup is positioned in CSS, above the highlight
|
|
|
|
popup.toggle( true ).toggleClipping( false ).togglePositioning( false );
|
|
|
|
|
|
|
|
// If the page is very short, there might not be enough space above the highlight,
|
|
|
|
// causing the popup to overlap the skin navigation or even be off-screen.
|
|
|
|
// Position it on top of the highlight in that case...
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
if ( popup.$popup[ 0 ].getBoundingClientRect().top < $( '.mw-body' )[ 0 ].getBoundingClientRect().top ) {
|
|
|
|
popup.$popup.addClass( 'ext-discussiontools-autotopicsubpopup-overlap' );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Scroll into view, leave some space above to avoid overlapping .postedit-container
|
|
|
|
OO.ui.Element.static.scrollIntoView(
|
|
|
|
popup.$popup[ 0 ],
|
|
|
|
{
|
|
|
|
padding: {
|
|
|
|
// Add padding to avoid overlapping the post-edit notification (above on desktop, below on mobile)
|
|
|
|
top: OO.ui.isMobile() ? 10 : 60,
|
|
|
|
bottom: OO.ui.isMobile() ? 85 : 10
|
|
|
|
},
|
|
|
|
// Specify scrollContainer for compatibility with MobileFrontend.
|
|
|
|
// Apparently it makes `<dd>` elements scrollable and OOUI tried to scroll them instead of body.
|
|
|
|
scrollContainer: OO.ui.Element.static.getRootScrollableElement( popup.$popup[ 0 ] )
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
popup.$element.addClass( 'ext-discussiontools-autotopicsubpopup-fadein' );
|
|
|
|
}
|
|
|
|
|
2021-08-19 20:35:32 +00:00
|
|
|
function updateSubscriptionStates( $container, headingsToUpdate ) {
|
|
|
|
// This method is called when we recently edited this page, and auto-subscriptions might have been
|
|
|
|
// added for some topics. It updates the [subscribe] buttons to reflect the new subscriptions.
|
|
|
|
|
|
|
|
var $links = $container.find( '.ext-discussiontools-init-section-subscribe-link' );
|
|
|
|
var linksByName = {};
|
|
|
|
$links.each( function ( i, elem ) {
|
|
|
|
linksByName[ elem.getAttribute( 'data-mw-comment-name' ) ] = elem;
|
|
|
|
} );
|
|
|
|
|
|
|
|
// If the topic is already marked as auto-subscribed, there's nothing to do.
|
2021-08-23 20:23:37 +00:00
|
|
|
// (Except maybe show the first-time popup.)
|
2021-08-19 20:35:32 +00:00
|
|
|
// If the topic is marked as having never been subscribed, check if they are auto-subscribed now.
|
|
|
|
var topicsToCheck = [];
|
|
|
|
var pending = [];
|
|
|
|
for ( var headingName in headingsToUpdate ) {
|
|
|
|
var el = linksByName[ headingName ];
|
|
|
|
var subscribedState = el.hasAttribute( 'data-mw-subscribed' ) ?
|
|
|
|
Number( el.getAttribute( 'data-mw-subscribed' ) ) : null;
|
|
|
|
|
2021-08-23 20:23:37 +00:00
|
|
|
if ( subscribedState === STATE_AUTOSUBSCRIBED ) {
|
|
|
|
maybeShowFirstTimeAutoTopicSubPopup();
|
|
|
|
} else if ( subscribedState === null ) {
|
2021-08-19 20:35:32 +00:00
|
|
|
topicsToCheck.push( headingName );
|
|
|
|
pending.push( el );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$( pending ).addClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
|
|
|
|
|
|
|
if ( !topicsToCheck.length ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var api = getApi();
|
|
|
|
api.get( {
|
|
|
|
action: 'discussiontoolsgetsubscriptions',
|
|
|
|
commentname: topicsToCheck
|
|
|
|
} ).then( function ( response ) {
|
|
|
|
if ( $.isEmptyObject( response.subscriptions ) ) {
|
|
|
|
// If none of the topics has an auto-subscription yet, wait a moment and check again.
|
|
|
|
// updateSubscriptionStates() method is only called if we're really expecting one to be there.
|
|
|
|
// (There are certainly neater ways to implement this, involving push notifications or at
|
|
|
|
// least long-polling or something. But this is the simplest one!)
|
|
|
|
var wait = $.Deferred();
|
|
|
|
setTimeout( wait.resolve, 5000 );
|
|
|
|
return wait.then( function () {
|
|
|
|
return api.get( {
|
|
|
|
action: 'discussiontoolsgetsubscriptions',
|
|
|
|
commentname: topicsToCheck
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
return response;
|
|
|
|
} ).then( function ( response ) {
|
|
|
|
// Update state of each topic for which there is a subscription
|
|
|
|
for ( var subItemName in response.subscriptions ) {
|
|
|
|
var state = response.subscriptions[ subItemName ];
|
|
|
|
updateSubscribeButton( linksByName[ subItemName ], state );
|
2021-08-23 20:23:37 +00:00
|
|
|
if ( state === STATE_AUTOSUBSCRIBED ) {
|
|
|
|
maybeShowFirstTimeAutoTopicSubPopup();
|
|
|
|
}
|
2021-08-19 20:35:32 +00:00
|
|
|
}
|
|
|
|
$( pending ).removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
|
|
|
}, function () {
|
|
|
|
$( pending ).removeClass( 'ext-discussiontools-init-section-subscribe-link-pending' );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
2021-04-29 18:03:37 +00:00
|
|
|
var $highlightedTarget = null;
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
function highlightTargetComment( parser, event ) {
|
|
|
|
// Delay with setTimeout() because "the Document's target element" (corresponding to the :target
|
|
|
|
// selector in CSS) is not yet updated to match the URL when handling a 'popstate' event.
|
|
|
|
setTimeout( function () {
|
|
|
|
if ( $highlightedTarget ) {
|
|
|
|
$highlightedTarget.remove();
|
|
|
|
$highlightedTarget = null;
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
var targetElement = $( ':target' )[ 0 ];
|
|
|
|
|
|
|
|
var uri;
|
|
|
|
try {
|
|
|
|
uri = new mw.Uri( location.href );
|
|
|
|
} catch ( err ) {
|
|
|
|
// T106244: URL encoded values using fallback 8-bit encoding (invalid UTF-8) cause mediawiki.Uri to crash
|
|
|
|
uri = null;
|
|
|
|
}
|
|
|
|
var targetIds = uri && uri.query.dtnewcomments && uri.query.dtnewcomments.split( '|' );
|
|
|
|
if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
|
|
|
|
var comment = parser.findCommentById( targetElement.getAttribute( 'id' ) );
|
|
|
|
$highlightedTarget = highlight( comment );
|
|
|
|
$highlightedTarget.addClass( 'ext-discussiontools-init-targetcomment' );
|
|
|
|
$highlightedTarget.addClass( 'ext-discussiontools-init-highlight-fadein' );
|
|
|
|
} else if ( targetIds ) {
|
|
|
|
var comments = targetIds.map( function ( id ) {
|
|
|
|
return parser.findCommentById( id );
|
|
|
|
} ).filter( function ( cmt ) {
|
|
|
|
return !!cmt;
|
|
|
|
} );
|
|
|
|
if ( comments.length === 0 ) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var highlights = comments.map( function ( cmt ) {
|
|
|
|
return highlight( cmt )[ 0 ];
|
|
|
|
} );
|
|
|
|
$highlightedTarget = $( highlights );
|
|
|
|
$highlightedTarget.addClass( 'ext-discussiontools-init-targetcomment' );
|
|
|
|
$highlightedTarget.addClass( 'ext-discussiontools-init-highlight-fadein' );
|
|
|
|
|
|
|
|
if ( !event ) {
|
|
|
|
// Scroll to the topmost comment on initial page load, but not on popstate events
|
2021-08-30 13:59:20 +00:00
|
|
|
var topmostComment = 0;
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
for ( var i = 1; i < comments.length; i++ ) {
|
2021-08-30 13:59:20 +00:00
|
|
|
if ( highlights[ i ].getBoundingClientRect().top < highlights[ topmostComment ].getBoundingClientRect().top ) {
|
|
|
|
topmostComment = i;
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
}
|
|
|
|
}
|
2021-08-30 13:59:20 +00:00
|
|
|
document.getElementById( comments[ topmostComment ].id ).scrollIntoView();
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
} );
|
2021-04-29 18:03:37 +00:00
|
|
|
}
|
|
|
|
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
function clearHighlightTargetComment( parser ) {
|
2021-07-12 19:41:04 +00:00
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
var targetElement = $( ':target' )[ 0 ];
|
|
|
|
if ( targetElement && targetElement.hasAttribute( 'data-mw-comment-start' ) ) {
|
|
|
|
// Clear the hash from the URL, triggering the 'hashchange' event and updating the :target
|
|
|
|
// selector (so that our code to clear our highlight works), but without scrolling anywhere.
|
|
|
|
// This is tricky because:
|
|
|
|
// * Using history.pushState() does not trigger 'hashchange' or update the :target selector.
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/History/pushState#description
|
|
|
|
// https://github.com/whatwg/html/issues/639
|
|
|
|
// * Using window.location.hash does, but it also scrolls to the target, which is the top of the
|
|
|
|
// page for the empty hash.
|
|
|
|
// Instead, we first use window.location.hash to navigate to a *different* hash (whose target
|
|
|
|
// doesn't exist on the page, hopefully), and then use history.pushState() to clear it.
|
|
|
|
window.location.hash += '-DoesNotExist-DiscussionToolsHack';
|
|
|
|
history.replaceState( null, document.title, window.location.href.replace( /#.*$/, '' ) );
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
} else if ( window.location.search.match( /(^\?|&)dtnewcomments=/ ) ) {
|
|
|
|
history.pushState( null, document.title,
|
|
|
|
window.location.search.replace( /(^\?|&)dtnewcomments=[^&]+/, '' ) + window.location.hash );
|
|
|
|
highlightTargetComment( parser );
|
2021-07-12 19:41:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-13 13:57:57 +00:00
|
|
|
function init( $container, state ) {
|
2021-06-24 16:21:31 +00:00
|
|
|
var
|
|
|
|
activeCommentId = null,
|
2021-01-25 22:21:34 +00:00
|
|
|
activeController = null,
|
2020-09-16 12:07:27 +00:00
|
|
|
// Loads later to avoid circular dependency
|
|
|
|
CommentController = require( './CommentController.js' ),
|
2020-08-29 12:00:51 +00:00
|
|
|
NewTopicController = require( './NewTopicController.js' ),
|
2020-11-18 18:44:44 +00:00
|
|
|
threadItemsById = {},
|
|
|
|
threadItems = [];
|
2019-11-05 13:55:01 +00:00
|
|
|
|
2020-12-15 19:45:05 +00:00
|
|
|
// Lazy-load postEdit module, may be required later (on desktop)
|
|
|
|
mw.loader.using( 'mediawiki.action.view.postEdit' );
|
|
|
|
|
2019-11-05 13:55:01 +00:00
|
|
|
$pageContainer = $container;
|
2021-06-24 16:21:31 +00:00
|
|
|
linksController = new ReplyLinksController( $pageContainer );
|
2021-04-08 13:46:09 +00:00
|
|
|
var parser = new Parser( $pageContainer[ 0 ] );
|
2019-11-24 15:39:52 +00:00
|
|
|
|
2021-04-08 13:46:09 +00:00
|
|
|
var pageThreads = [];
|
|
|
|
var commentNodes = $pageContainer[ 0 ].querySelectorAll( '[data-mw-comment]' );
|
2020-11-18 18:44:44 +00:00
|
|
|
threadItems.length = commentNodes.length;
|
2020-04-29 16:43:11 +00:00
|
|
|
|
2020-10-21 15:52:04 +00:00
|
|
|
// The page can be served from the HTTP cache (Varnish), containing data-mw-comment generated
|
|
|
|
// by an older version of our PHP code. Code below must be able to handle that.
|
2021-04-19 18:34:55 +00:00
|
|
|
// See CommentFormatter::addDiscussionTools() in PHP.
|
2020-10-21 15:52:04 +00:00
|
|
|
|
2020-09-16 12:07:27 +00:00
|
|
|
// Iterate over commentNodes backwards so replies are always deserialized before their parents.
|
2021-04-08 13:46:09 +00:00
|
|
|
var i, comment;
|
2020-09-16 12:07:27 +00:00
|
|
|
for ( i = commentNodes.length - 1; i >= 0; i-- ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var hash = JSON.parse( commentNodes[ i ].getAttribute( 'data-mw-comment' ) );
|
2020-10-27 12:18:50 +00:00
|
|
|
comment = ThreadItem.static.newFromJSON( hash, threadItemsById );
|
2021-02-12 19:16:13 +00:00
|
|
|
if ( !comment.name ) {
|
|
|
|
comment.name = parser.computeName( comment );
|
|
|
|
}
|
|
|
|
|
2020-10-21 15:52:04 +00:00
|
|
|
threadItemsById[ comment.id ] = comment;
|
2020-09-16 12:07:27 +00:00
|
|
|
|
2021-06-24 16:21:31 +00:00
|
|
|
if ( comment.type === 'heading' ) {
|
2020-09-16 12:07:27 +00:00
|
|
|
// Use unshift as we are in a backwards loop
|
|
|
|
pageThreads.unshift( comment );
|
|
|
|
}
|
2020-11-18 18:44:44 +00:00
|
|
|
threadItems[ i ] = comment;
|
2020-09-16 12:07:27 +00:00
|
|
|
}
|
2019-11-05 13:55:01 +00:00
|
|
|
|
2020-10-21 15:52:04 +00:00
|
|
|
// Recalculate legacy IDs
|
2021-02-12 19:16:13 +00:00
|
|
|
parser.threadItemsByName = {};
|
2020-10-21 15:52:04 +00:00
|
|
|
parser.threadItemsById = {};
|
2020-11-18 18:44:44 +00:00
|
|
|
// In the forward order this time, as the IDs for indistinguishable comments depend on it
|
|
|
|
for ( i = 0; i < threadItems.length; i++ ) {
|
|
|
|
comment = threadItems[ i ];
|
2021-02-12 19:16:13 +00:00
|
|
|
|
|
|
|
if ( !parser.threadItemsByName[ comment.name ] ) {
|
|
|
|
parser.threadItemsByName[ comment.name ] = [];
|
|
|
|
}
|
|
|
|
parser.threadItemsByName[ comment.name ].push( comment );
|
|
|
|
|
2021-04-08 13:46:09 +00:00
|
|
|
var newId = parser.computeId( comment );
|
2020-11-18 18:44:44 +00:00
|
|
|
parser.threadItemsById[ newId ] = comment;
|
|
|
|
if ( newId !== comment.id ) {
|
2020-10-21 15:52:04 +00:00
|
|
|
comment.id = newId;
|
|
|
|
threadItemsById[ newId ] = comment;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 19:59:47 +00:00
|
|
|
if ( featuresEnabled.topicsubscription ) {
|
2021-02-17 22:34:02 +00:00
|
|
|
initTopicSubscriptions( $container );
|
|
|
|
}
|
|
|
|
|
2021-06-24 16:21:31 +00:00
|
|
|
function setupController( commentId, $link, mode, hideErrors ) {
|
2021-08-05 19:05:29 +00:00
|
|
|
var commentController, $addSectionLink;
|
2021-06-24 16:21:31 +00:00
|
|
|
if ( commentId === utils.NEW_TOPIC_COMMENT_ID ) {
|
2021-08-05 19:05:29 +00:00
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
$addSectionLink = $( '#ca-addsection a' );
|
|
|
|
// When opening new topic tool using any link, always activate the link in page tabs too
|
|
|
|
$link = $link.add( $addSectionLink );
|
2021-06-24 16:21:31 +00:00
|
|
|
commentController = new NewTopicController( $pageContainer, parser );
|
|
|
|
} else {
|
|
|
|
commentController = new CommentController( $pageContainer, parser.findCommentById( commentId ), parser );
|
|
|
|
}
|
|
|
|
|
|
|
|
activeCommentId = commentId;
|
|
|
|
activeController = commentController;
|
|
|
|
linksController.setActiveLink( $link );
|
|
|
|
|
|
|
|
commentController.on( 'teardown', function ( abandoned ) {
|
|
|
|
activeCommentId = null;
|
|
|
|
activeController = null;
|
|
|
|
linksController.clearActiveLink();
|
|
|
|
|
|
|
|
if ( abandoned ) {
|
|
|
|
linksController.focusLink( $link );
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
|
|
|
|
commentController.setup( mode, hideErrors );
|
|
|
|
}
|
|
|
|
|
2021-01-25 22:21:34 +00:00
|
|
|
// Hook up each link to open a reply widget
|
|
|
|
//
|
|
|
|
// TODO: Allow users to use multiple reply widgets simultaneously.
|
|
|
|
// Currently submitting a reply from one widget would also destroy the other ones.
|
2021-06-24 16:21:31 +00:00
|
|
|
linksController.on( 'link-click', function ( commentId, $link ) {
|
|
|
|
// If the reply widget is already open, activate it.
|
|
|
|
// Reply links are also made unclickable using 'pointer-events' in CSS, but that doesn't happen
|
|
|
|
// for new section links, because we don't have a good way of visually disabling them.
|
|
|
|
// (And it also doesn't work on IE 11.)
|
|
|
|
if ( activeCommentId === commentId ) {
|
|
|
|
activeController.showAndFocus();
|
|
|
|
return;
|
|
|
|
}
|
2021-01-25 22:21:34 +00:00
|
|
|
|
2021-06-24 16:21:31 +00:00
|
|
|
// If this is a new topic link, and a reply widget is open, attempt to close it first.
|
2021-11-08 19:03:40 +00:00
|
|
|
var teardownPromise;
|
2021-06-24 16:21:31 +00:00
|
|
|
if ( activeController && commentId === utils.NEW_TOPIC_COMMENT_ID ) {
|
2021-11-08 19:03:40 +00:00
|
|
|
teardownPromise = activeController.replyWidget.tryTeardown();
|
2021-06-24 16:21:31 +00:00
|
|
|
} else {
|
2021-11-08 19:03:40 +00:00
|
|
|
teardownPromise = $.Deferred().resolve();
|
2021-06-24 16:21:31 +00:00
|
|
|
}
|
2021-01-25 22:41:35 +00:00
|
|
|
|
2021-11-08 19:03:40 +00:00
|
|
|
teardownPromise.then( function () {
|
2021-01-25 22:21:34 +00:00
|
|
|
// If another reply widget is open (or opening), do nothing.
|
|
|
|
if ( activeController ) {
|
|
|
|
return;
|
|
|
|
}
|
2021-06-24 16:21:31 +00:00
|
|
|
setupController( commentId, $link );
|
2021-01-25 22:21:34 +00:00
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
2021-06-24 16:21:31 +00:00
|
|
|
// Restore autosave
|
2021-11-08 19:03:40 +00:00
|
|
|
( function () {
|
|
|
|
var mode, $link;
|
|
|
|
for ( i = 0; i < threadItems.length; i++ ) {
|
|
|
|
comment = threadItems[ i ];
|
|
|
|
if ( storage.get( 'reply/' + comment.id + '/saveable' ) ) {
|
|
|
|
mode = storage.get( 'reply/' + comment.id + '/mode' );
|
|
|
|
$link = $( commentNodes[ i ] );
|
|
|
|
setupController( comment.id, $link, mode, true );
|
|
|
|
break;
|
|
|
|
}
|
2021-06-24 16:21:31 +00:00
|
|
|
}
|
2021-11-08 19:03:40 +00:00
|
|
|
if ( storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/saveable' ) ) {
|
|
|
|
mode = storage.get( 'reply/' + utils.NEW_TOPIC_COMMENT_ID + '/mode' );
|
|
|
|
setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ), mode, true );
|
|
|
|
} else if ( mw.config.get( 'wgDiscussionToolsStartNewTopicTool' ) ) {
|
|
|
|
setupController( utils.NEW_TOPIC_COMMENT_ID, $( [] ) );
|
|
|
|
}
|
|
|
|
}() );
|
2021-06-24 16:21:31 +00:00
|
|
|
|
2020-09-16 12:07:27 +00:00
|
|
|
// For debugging (now unused in the code)
|
2019-11-05 13:55:01 +00:00
|
|
|
mw.dt.pageThreads = pageThreads;
|
|
|
|
|
2021-06-17 15:37:05 +00:00
|
|
|
var promise = OO.ui.isMobile && mw.loader.getState( 'mobile.init' ) ?
|
|
|
|
mw.loader.using( 'mobile.init' ) :
|
|
|
|
$.Deferred().resolve().promise();
|
|
|
|
|
|
|
|
promise.then( function () {
|
|
|
|
var $highlight;
|
|
|
|
if ( state.repliedTo === utils.NEW_TOPIC_COMMENT_ID ) {
|
|
|
|
// Highlight the last comment on the page
|
|
|
|
var lastComment = threadItems[ threadItems.length - 1 ];
|
|
|
|
$highlight = highlight( lastComment );
|
2021-08-23 20:23:37 +00:00
|
|
|
lastHighlightComment = lastComment;
|
2021-06-17 15:37:05 +00:00
|
|
|
|
|
|
|
// If it's the only comment under its heading, highlight the heading too.
|
|
|
|
// (It might not be if the new discussion topic was posted without a title: T272666.)
|
|
|
|
if (
|
|
|
|
lastComment.parent &&
|
|
|
|
lastComment.parent.type === 'heading' &&
|
|
|
|
lastComment.parent.replies.length === 1
|
|
|
|
) {
|
|
|
|
$highlight = $highlight.add( highlight( lastComment.parent ) );
|
2021-08-23 20:23:37 +00:00
|
|
|
lastHighlightComment = lastComment.parent;
|
2021-06-17 15:37:05 +00:00
|
|
|
}
|
2019-11-13 13:57:57 +00:00
|
|
|
|
2020-12-15 19:45:05 +00:00
|
|
|
mw.hook( 'postEdit' ).fire( {
|
2021-06-17 15:37:05 +00:00
|
|
|
message: mw.msg( 'discussiontools-postedit-confirmation-topicadded', mw.user )
|
2020-12-15 19:45:05 +00:00
|
|
|
} );
|
2021-06-17 15:37:05 +00:00
|
|
|
|
|
|
|
} else if ( state.repliedTo ) {
|
|
|
|
// Find the comment we replied to, then highlight the last reply
|
|
|
|
var repliedToComment = threadItemsById[ state.repliedTo ];
|
|
|
|
$highlight = highlight( repliedToComment.replies[ repliedToComment.replies.length - 1 ] );
|
2021-08-23 20:23:37 +00:00
|
|
|
lastHighlightComment = repliedToComment.replies[ repliedToComment.replies.length - 1 ];
|
2021-06-17 15:37:05 +00:00
|
|
|
|
|
|
|
if ( OO.ui.isMobile() ) {
|
|
|
|
mw.notify( mw.msg( 'discussiontools-postedit-confirmation-published', mw.user ) );
|
|
|
|
} else {
|
|
|
|
// postEdit is currently desktop only
|
|
|
|
mw.hook( 'postEdit' ).fire( {
|
|
|
|
message: mw.msg( 'discussiontools-postedit-confirmation-published', mw.user )
|
|
|
|
} );
|
|
|
|
}
|
2020-12-15 19:45:05 +00:00
|
|
|
}
|
2020-11-30 23:45:15 +00:00
|
|
|
|
2021-06-17 15:37:05 +00:00
|
|
|
if ( $highlight ) {
|
|
|
|
$highlight.addClass( 'ext-discussiontools-init-publishedcomment' );
|
2021-04-29 18:03:37 +00:00
|
|
|
|
2021-06-17 15:37:05 +00:00
|
|
|
// Show a highlight with the same timing as the post-edit message (mediawiki.action.view.postEdit):
|
|
|
|
// show for 3000ms, fade out for 250ms (animation duration is defined in CSS).
|
|
|
|
OO.ui.Element.static.scrollIntoView(
|
|
|
|
$highlight[ 0 ],
|
|
|
|
{
|
|
|
|
padding: {
|
2021-08-23 20:23:37 +00:00
|
|
|
// Add padding to avoid overlapping the post-edit notification (above on desktop, below on mobile)
|
|
|
|
top: OO.ui.isMobile() ? 10 : 60,
|
|
|
|
bottom: OO.ui.isMobile() ? 85 : 10
|
2021-06-17 15:37:05 +00:00
|
|
|
},
|
|
|
|
// Specify scrollContainer for compatibility with MobileFrontend.
|
|
|
|
// Apparently it makes `<dd>` elements scrollable and OOUI tried to scroll them instead of body.
|
|
|
|
scrollContainer: OO.ui.Element.static.getRootScrollableElement( $highlight[ 0 ] )
|
|
|
|
}
|
|
|
|
).then( function () {
|
|
|
|
$highlight.addClass( 'ext-discussiontools-init-highlight-fadein' );
|
2021-04-29 18:03:37 +00:00
|
|
|
setTimeout( function () {
|
2021-06-17 15:37:05 +00:00
|
|
|
$highlight.addClass( 'ext-discussiontools-init-highlight-fadeout' );
|
|
|
|
setTimeout( function () {
|
|
|
|
// Remove the node when no longer needed, because it's using CSS 'mix-blend-mode', which
|
|
|
|
// affects the text rendering of the whole page, disabling subpixel antialiasing on Windows
|
|
|
|
$highlight.remove();
|
|
|
|
}, 250 );
|
|
|
|
}, 3000 );
|
|
|
|
} );
|
|
|
|
}
|
|
|
|
} );
|
2021-04-29 18:03:37 +00:00
|
|
|
|
2021-08-19 20:35:32 +00:00
|
|
|
// Check topic subscription states if the user has automatic subscriptions enabled
|
|
|
|
// and has recently edited this page.
|
|
|
|
if ( featuresEnabled.autotopicsub && mw.user.options.get( 'discussiontools-autotopicsub' ) ) {
|
|
|
|
var recentComments = [];
|
|
|
|
var headingsToUpdate = {};
|
|
|
|
if ( state.repliedTo ) {
|
|
|
|
// Edited by using the reply tool or new topic tool. Only check the edited topic.
|
|
|
|
if ( state.repliedTo === utils.NEW_TOPIC_COMMENT_ID ) {
|
|
|
|
recentComments.push( threadItems[ threadItems.length - 1 ] );
|
|
|
|
} else {
|
|
|
|
recentComments.push( threadItemsById[ state.repliedTo ] );
|
|
|
|
}
|
|
|
|
} else if ( mw.config.get( 'wgPostEdit' ) ) {
|
|
|
|
// Edited by using wikitext editor. Check topics with their own comments within last minute.
|
|
|
|
for ( i = 0; i < threadItems.length; i++ ) {
|
|
|
|
if (
|
|
|
|
threadItems[ i ] instanceof CommentItem &&
|
|
|
|
threadItems[ i ].author === mw.user.getName() &&
|
|
|
|
threadItems[ i ].timestamp.isSameOrAfter( moment().subtract( 1, 'minute' ), 'minute' )
|
|
|
|
) {
|
|
|
|
recentComments.push( threadItems[ i ] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for ( i = 0; i < recentComments.length; i++ ) {
|
|
|
|
var headingItem = recentComments[ i ].getHeading();
|
|
|
|
while ( headingItem instanceof HeadingItem && headingItem.headingLevel !== 2 ) {
|
|
|
|
headingItem = headingItem.parent;
|
|
|
|
}
|
|
|
|
// Use names as object keys to deduplicate if there are multiple comments in a topic.
|
|
|
|
headingsToUpdate[ headingItem.name ] = headingItem;
|
|
|
|
}
|
|
|
|
updateSubscriptionStates( $container, headingsToUpdate );
|
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
// Preload page metadata.
|
2020-02-15 04:03:18 +00:00
|
|
|
// TODO: Isn't this too early to load it? We will only need it if the user tries replying...
|
|
|
|
getPageData(
|
|
|
|
mw.config.get( 'wgRelevantPageName' ),
|
2020-02-15 04:49:58 +00:00
|
|
|
mw.config.get( 'wgCurRevisionId' )
|
2020-02-15 04:03:18 +00:00
|
|
|
);
|
2021-04-29 18:03:37 +00:00
|
|
|
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
$( window ).on( 'popstate', highlightTargetComment.bind( null, parser ) );
|
2021-07-12 19:41:04 +00:00
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
$( 'body' ).on( 'click', function ( e ) {
|
|
|
|
if ( e.which === OO.ui.MouseButtons.LEFT ) {
|
Handle highlighting and scrolling to comments for bundled notifications
Notifications are bundled by section, so instead of linking to the
comment, link to the section.
Additionally, add a parameter to the URL listing all the comment IDs
from the bundle, and highlight them all and scroll to the topmost one.
Having to handle both URL fragments and URL query parameters makes
this code kind of a mess :(
Also, some unexpected changes became necessary:
* EventDispatcher.php: Store the section title in events using
HeadingItem::getLinkableTitle() instead of ThreadItem::getText().
The result is mostly the same, except in case of wacky markup like
images or extension tags. We can more reliably use it to link to the
section on the page, and we already use getLinkableTitle() when
generating edit summaries in the reply tool for this reason.
* dt.init.less: Change the mix-blend-mode for the highlights from
'multiply' to 'darken', so that multiple overlapping highlights do
not look more opaque. This affects how the highlights look on
non-white backgrounds and images (they're less blue, and on darker
backgrounds entirely invisible), but it seems worth it.
Bug: T286620
Change-Id: I21bb5c003abc2747f0350d3f3af558dfb55693e9
2021-08-05 20:04:17 +00:00
|
|
|
clearHighlightTargetComment( parser );
|
2021-07-12 19:41:04 +00:00
|
|
|
}
|
|
|
|
} );
|
2021-04-29 18:03:37 +00:00
|
|
|
highlightTargetComment( parser );
|
2019-11-05 13:55:01 +00:00
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
function update( data, comment, pageName, replyWidget ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var api = getApi(),
|
2021-07-22 17:59:18 +00:00
|
|
|
pageUpdated = $.Deferred(),
|
|
|
|
$content;
|
2020-08-19 20:03:41 +00:00
|
|
|
|
2020-10-22 19:52:05 +00:00
|
|
|
// We posted a new comment, clear the cache, because wgCurRevisionId will not change if we posted
|
|
|
|
// to a transcluded page (T266275)
|
|
|
|
pageDataCache[ mw.config.get( 'wgRelevantPageName' ) ][ mw.config.get( 'wgCurRevisionId' ) ] = null;
|
|
|
|
|
2021-07-29 06:12:10 +00:00
|
|
|
var pageExists = !!mw.config.get( 'wgRelevantArticleId' );
|
|
|
|
if ( !pageExists ) {
|
|
|
|
// The page didn't exist before this update, so reload it. We'd handle
|
|
|
|
// setting up the content just fine (assuming there's a
|
|
|
|
// mw-parser-output), but fixing up the UI tabs/behavior is outside
|
|
|
|
// our scope.
|
2021-08-06 00:31:50 +00:00
|
|
|
replyWidget.unbindBeforeUnloadHandler();
|
|
|
|
replyWidget.clearStorage();
|
|
|
|
replyWidget.setPending( true );
|
2021-07-29 06:12:10 +00:00
|
|
|
window.location = mw.util.getUrl( pageName, { dtrepliedto: comment.id } );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-08-06 00:31:50 +00:00
|
|
|
replyWidget.teardown();
|
|
|
|
linksController.teardown();
|
|
|
|
linksController = null;
|
|
|
|
// TODO: Tell controller to teardown all other open widgets
|
|
|
|
|
2021-06-17 15:37:05 +00:00
|
|
|
if ( OO.ui.isMobile() ) {
|
|
|
|
// MobileFrontend does not use the 'wikipage.content' hook, and its interface will not
|
|
|
|
// re-initialize properly (e.g. page sections won't be collapsible). Reload the whole page.
|
|
|
|
window.location = mw.util.getUrl( pageName, { dtrepliedto: comment.id } );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
// Update page state
|
|
|
|
if ( pageName === mw.config.get( 'wgRelevantPageName' ) ) {
|
|
|
|
// We can use the result from the VisualEditor API
|
2021-07-22 17:59:18 +00:00
|
|
|
$content = $( $.parseHTML( data.content ) );
|
|
|
|
$pageContainer.find( '.mw-parser-output' ).replaceWith( $content );
|
2020-08-19 20:03:41 +00:00
|
|
|
mw.config.set( {
|
|
|
|
wgCurRevisionId: data.newrevid,
|
|
|
|
wgRevisionId: data.newrevid
|
|
|
|
} );
|
|
|
|
mw.config.set( data.jsconfigvars );
|
|
|
|
// Note: VE API merges 'modules' and 'modulestyles'
|
|
|
|
mw.loader.load( data.modules );
|
|
|
|
// TODO update categories, displaytitle, lastmodified
|
|
|
|
// (see ve.init.mw.DesktopArticleTarget.prototype.replacePageContent)
|
|
|
|
|
|
|
|
pageUpdated.resolve();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// We saved to another page, we must purge and then fetch the current page
|
|
|
|
api.post( {
|
|
|
|
action: 'purge',
|
|
|
|
titles: mw.config.get( 'wgRelevantPageName' )
|
|
|
|
} ).then( function () {
|
|
|
|
return api.get( {
|
|
|
|
action: 'parse',
|
2020-10-22 13:03:40 +00:00
|
|
|
// HACK: 'useskin' triggers a different code path that runs our OutputPageBeforeHTML hook,
|
|
|
|
// adding our reply links in the HTML (T266195)
|
|
|
|
useskin: mw.config.get( 'skin' ),
|
2020-10-23 11:02:18 +00:00
|
|
|
uselang: mw.config.get( 'wgUserLanguage' ),
|
2020-11-18 18:19:06 +00:00
|
|
|
// HACK: Always display reply links afterwards, ignoring preferences etc., in case this was
|
|
|
|
// a page view with reply links forced with ?dtenable=1 or otherwise
|
|
|
|
dtenable: '1',
|
2020-08-19 20:03:41 +00:00
|
|
|
prop: [ 'text', 'modules', 'jsconfigvars' ],
|
|
|
|
page: mw.config.get( 'wgRelevantPageName' )
|
|
|
|
} );
|
|
|
|
} ).then( function ( parseResp ) {
|
2021-07-22 17:59:18 +00:00
|
|
|
$content = $( $.parseHTML( parseResp.parse.text ) );
|
|
|
|
$pageContainer.find( '.mw-parser-output' ).replaceWith( $content );
|
2020-08-19 20:03:41 +00:00
|
|
|
mw.config.set( parseResp.parse.jsconfigvars );
|
|
|
|
mw.loader.load( parseResp.parse.modulestyles );
|
|
|
|
mw.loader.load( parseResp.parse.modules );
|
|
|
|
// TODO update categories, displaytitle, lastmodified
|
|
|
|
// We may not be able to use prop=displaytitle without making changes in the action=parse API,
|
|
|
|
// VE API has some confusing code that changes the HTML escaping on it before returning???
|
|
|
|
|
|
|
|
pageUpdated.resolve();
|
|
|
|
|
|
|
|
} ).catch( function () {
|
|
|
|
// We saved the reply, but couldn't purge or fetch the updated page. Seems difficult to
|
|
|
|
// explain this problem. Redirect to the page where the user can at least see their reply…
|
2021-06-17 15:37:05 +00:00
|
|
|
window.location = mw.util.getUrl( pageName, { dtrepliedto: comment.id } );
|
2020-08-19 20:03:41 +00:00
|
|
|
} );
|
|
|
|
}
|
|
|
|
|
|
|
|
// User logged in if module loaded.
|
|
|
|
if ( mw.loader.getState( 'mediawiki.page.watch.ajax' ) === 'ready' ) {
|
2021-04-08 13:46:09 +00:00
|
|
|
var watch = require( 'mediawiki.page.watch.ajax' );
|
2020-08-28 14:50:42 +00:00
|
|
|
|
2020-08-19 20:03:41 +00:00
|
|
|
watch.updateWatchLink(
|
|
|
|
// eslint-disable-next-line no-jquery/no-global-selector
|
|
|
|
$( '#ca-watch a, #ca-unwatch a' ),
|
2020-08-28 14:50:42 +00:00
|
|
|
data.watched ? 'unwatch' : 'watch',
|
|
|
|
'idle',
|
|
|
|
data.watchlistexpiry
|
2020-08-19 20:03:41 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
pageUpdated.then( function () {
|
|
|
|
// Re-initialize and highlight the new reply.
|
|
|
|
mw.dt.initState.repliedTo = comment.id;
|
|
|
|
|
|
|
|
// We need our init code to run after everyone else's handlers for this hook,
|
|
|
|
// so that all changes to the page layout have been completed (e.g. collapsible elements),
|
|
|
|
// and we can measure things and display the highlight in the right place.
|
|
|
|
mw.hook( 'wikipage.content' ).remove( mw.dt.init );
|
|
|
|
mw.hook( 'wikipage.content' ).fire( $pageContainer );
|
|
|
|
// The hooks have "memory" so calling add() after fire() actually fires the handler,
|
|
|
|
// and calling add() before fire() would actually fire it twice.
|
|
|
|
mw.hook( 'wikipage.content' ).add( mw.dt.init );
|
|
|
|
|
|
|
|
logger( {
|
|
|
|
action: 'saveSuccess',
|
2021-09-13 23:35:43 +00:00
|
|
|
timing: mw.now() - replyWidget.saveInitiated,
|
2020-08-19 20:03:41 +00:00
|
|
|
// eslint-disable-next-line camelcase
|
|
|
|
revision_id: data.newrevid
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-11-05 13:55:01 +00:00
|
|
|
module.exports = {
|
|
|
|
init: init,
|
2020-08-19 20:03:41 +00:00
|
|
|
update: update,
|
|
|
|
checkCommentOnPage: checkCommentOnPage,
|
2020-10-23 11:02:18 +00:00
|
|
|
getCheckboxesPromise: getCheckboxesPromise,
|
|
|
|
getApi: getApi
|
2019-11-05 13:55:01 +00:00
|
|
|
};
|