diff --git a/.eslintignore b/.eslintignore
index 7a54349e9..49f860025 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -3,6 +3,7 @@
# Build
/vendor/
+/coverage/
# Language files written automatically by TranslateWiki
/i18n/**/*.json
diff --git a/extension.json b/extension.json
index eff942639..28587c887 100644
--- a/extension.json
+++ b/extension.json
@@ -42,6 +42,7 @@
"highlighter.js",
"topicsubscriptions.js",
"mobile.js",
+ "LedeSectionDialog.js",
{
"name": "controller/contLangMessages.json",
"callback": "\\MediaWiki\\Extension\\DiscussionTools\\ResourceLoaderData::getContentLanguageMessages",
@@ -118,6 +119,7 @@
"discussiontools-error-noswitchtove-table",
"discussiontools-error-noswitchtove-template",
"discussiontools-error-noswitchtove-title",
+ "discussiontools-ledesection-title",
"discussiontools-newtopic-legacy-hint",
"discussiontools-newtopic-placeholder-title",
"discussiontools-newtopic-missing-title",
diff --git a/i18n/en.json b/i18n/en.json
index 86576dbf3..fe2db8ee7 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -44,6 +44,8 @@
"discussiontools-findcomment-results-notcurrent": "(not in current revision)",
"discussiontools-findcomment-results-transcluded": "(transcluded from another page)",
"discussiontools-findcomment-title": "Find comment",
+ "discussiontools-ledesection-button": "Learn more about this page",
+ "discussiontools-ledesection-title": "About this talk page",
"discussiontools-limitreport-errorreqid": "DiscussionTools error request ID",
"discussiontools-limitreport-timeusage": "DiscussionTools time usage",
"discussiontools-limitreport-timeusage-value": "$1 {{PLURAL:$1|second|seconds}}",
diff --git a/i18n/qqq.json b/i18n/qqq.json
index 33d005110..ece742e41 100644
--- a/i18n/qqq.json
+++ b/i18n/qqq.json
@@ -57,6 +57,8 @@
"discussiontools-findcomment-results-notcurrent": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.",
"discussiontools-findcomment-results-transcluded": "Additional label for a result on [[Special:FindComment]], shown following a link to a page.",
"discussiontools-findcomment-title": "Page title for [[Special:FindComment]], also shown on the list on [[Special:SpecialPages]]",
+ "discussiontools-ledesection-button": "Label for button to reveal content from the lede section (the section above the first heading).",
+ "discussiontools-ledesection-title": "Title of dialog showing from the lede section (the section above the first heading).",
"discussiontools-limitreport-errorreqid": "Label for the ID of the web request in which a DiscussionTools error has occurred.",
"discussiontools-limitreport-timeusage": "Label for the time usage (duration) of DiscussionTools in the parser limit report. Followed by {{msg-mw|discussiontools-limitreport-timeusage-value}}.\n\nSimilar to:\n* {{msg-mw|limitreport-cputime}}\n* {{msg-mw|limitreport-walltime}}\n* {{msg-mw|scribunto-limitreport-timeusage}}",
"discussiontools-limitreport-timeusage-value": "Follows {{msg-mw|discussiontools-limitreport-timeusage}}.\n\nParameters:\n* $1 - the usage in seconds\n{{Identical|Second}}",
diff --git a/includes/CommentFormatter.php b/includes/CommentFormatter.php
index 568ed64d9..bae5e8b04 100644
--- a/includes/CommentFormatter.php
+++ b/includes/CommentFormatter.php
@@ -306,6 +306,8 @@ class CommentFormatter {
$container->appendChild( $newestCommentMarker );
}
+ $firstHeading = null;
+
// Enhance other
's which aren't part of a thread
$headings = DOMCompat::querySelectorAll( $container, 'h2' );
foreach ( $headings as $headingElement ) {
@@ -314,6 +316,18 @@ class CommentFormatter {
continue;
}
static::addTopicContainer( $headingElement );
+ if ( !$firstHeading ) {
+ $firstHeading = $headingElement;
+ }
+ }
+
+ if (
+ // Page has no headings but some content
+ ( !$firstHeading && $container->childNodes->length ) ||
+ // Page has content before the first heading
+ ( $firstHeading && $firstHeading->previousSibling !== null )
+ ) {
+ $container->appendChild( $doc->createComment( '__DTHASLEDECONTENT__' ) );
}
if ( count( $threadItems ) === 0 ) {
@@ -760,4 +774,14 @@ class CommentFormatter {
return str_replace( '', $content, $text );
}
+ /**
+ * Check if the talk page has content above the first heading, in the lede section.
+ *
+ * @param string $text
+ * @return bool
+ */
+ public static function hasLedeContent( string $text ): bool {
+ return strpos( $text, '' ) !== false;
+ }
+
}
diff --git a/includes/Hooks/PageHooks.php b/includes/Hooks/PageHooks.php
index 90529c441..f96fb299f 100644
--- a/includes/Hooks/PageHooks.php
+++ b/includes/Hooks/PageHooks.php
@@ -197,12 +197,14 @@ class PageHooks implements
$text, $lang, $this->subscriptionStore, $output->getUser(), $isMobile
);
}
+
if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) ) {
$output->enableOOUI();
$text = CommentFormatter::postprocessReplyTool(
$text, $lang, $isMobile
);
}
+
if (
CommentFormatter::isEmptyTalkPage( $text ) &&
HookUtils::shouldDisplayEmptyState( $output->getContext() )
@@ -213,6 +215,26 @@ class PageHooks implements
);
$output->addBodyClasses( 'ext-discussiontools-emptystate-shown' );
}
+
+ if (
+ $output->getSkin()->getSkinName() === 'minerva' &&
+ CommentFormatter::hasLedeContent( $text )
+ ) {
+ $output->enableOOUI();
+ $output->addHTML(
+ Html::rawElement( 'div',
+ [ 'class' => 'ext-discussiontools-init-lede-button-container' ],
+ ( new ButtonWidget( [
+ 'label' => $output->getContext()->msg( 'discussiontools-ledesection-button' )->text(),
+ 'classes' => [ 'ext-discussiontools-init-lede-button' ],
+ 'framed' => false,
+ 'icon' => 'info',
+ 'infusable' => true,
+ ] ) )
+ )
+ );
+ }
+
if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) {
$output->enableOOUI();
if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) {
@@ -397,28 +419,34 @@ class PageHooks implements
* @return bool|void
*/
public function onArticleViewHeader( $article, &$outputDone, &$pcache ) {
- $title = $article->getTitle();
$context = $article->getContext();
$output = $context->getOutput();
- if (
- $output->getSkin()->getSkinName() === 'minerva' &&
- HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) &&
- // Only add the button if "New section" tab would be shown in a normal skin.
- HookUtils::shouldShowNewSectionTab( $context )
- ) {
- // Minerva doesn't show a new topic button by default, unless the MobileFrontend
- // talk page feature is enabled, but we shouldn't depend on code from there.
- $output->enableOOUI();
- $output->addHTML(
- ( new ButtonWidget( [
- 'href' => $title->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ),
- 'label' => $context->msg( 'skin-action-addsection' )->text(),
- 'flags' => [ 'progressive', 'primary' ],
- 'classes' => [ 'ext-discussiontools-init-new-topic' ]
- ] ) )
- // For compatibility with Minerva click tracking (T295490)
- ->setAttributes( [ 'data-event-name' => 'talkpage.add-topic' ] )
- );
+
+ if ( $output->getSkin()->getSkinName() === 'minerva' ) {
+ $title = $article->getTitle();
+
+ if (
+ HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) &&
+ // Only add the button if "New section" tab would be shown in a normal skin.
+ HookUtils::shouldShowNewSectionTab( $context )
+ ) {
+ $output->enableOOUI();
+
+ // Minerva doesn't show a new topic button by default, unless the MobileFrontend
+ // talk page feature is enabled, but we shouldn't depend on code from there.
+ $output->addHTML(
+ Html::rawElement( 'div',
+ [ 'class' => 'ext-discussiontools-init-new-topic' ],
+ ( new ButtonWidget( [
+ 'href' => $title->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ),
+ 'label' => $context->msg( 'skin-action-addsection' )->text(),
+ 'flags' => [ 'progressive', 'primary' ],
+ ] ) )
+ // For compatibility with Minerva click tracking (T295490)
+ ->setAttributes( [ 'data-event-name' => 'talkpage.add-topic' ] )
+ )
+ );
+ }
}
}
diff --git a/modules/LedeSectionDialog.js b/modules/LedeSectionDialog.js
new file mode 100644
index 000000000..945cc840e
--- /dev/null
+++ b/modules/LedeSectionDialog.js
@@ -0,0 +1,43 @@
+function LedeSectionDialog() {
+ // Parent constructor
+ LedeSectionDialog.super.apply( this, arguments );
+}
+
+/* Inheritance */
+OO.inheritClass( LedeSectionDialog, OO.ui.ProcessDialog );
+
+LedeSectionDialog.static.name = 'ledeSection';
+
+LedeSectionDialog.static.size = 'larger';
+
+LedeSectionDialog.static.title = OO.ui.deferMsg( 'discussiontools-ledesection-title' );
+
+LedeSectionDialog.static.actions = [
+ {
+ label: OO.ui.deferMsg( 'visualeditor-dialog-action-done' ),
+ flags: [ 'safe', 'close' ]
+ }
+];
+
+LedeSectionDialog.prototype.initialize = function () {
+ // Parent method
+ LedeSectionDialog.super.prototype.initialize.call( this );
+
+ this.contentLayout = new OO.ui.PanelLayout( {
+ scrollable: true,
+ padded: true,
+ expanded: false,
+ classes: [ 'ext-discussiontools-ui-ledeSectionDialog-content', 'mw-parser-output', 'content' ]
+ } );
+
+ this.$body.append( this.contentLayout.$element );
+};
+
+LedeSectionDialog.prototype.getSetupProcess = function ( data ) {
+ return LedeSectionDialog.super.prototype.getSetupProcess.call( this, data )
+ .next( function () {
+ this.contentLayout.$element.empty().append( data.$content );
+ }, this );
+};
+
+module.exports = LedeSectionDialog;
diff --git a/modules/dt.init.less b/modules/dt.init.less
index abd5f3ae9..ec1abfabc 100644
--- a/modules/dt.init.less
+++ b/modules/dt.init.less
@@ -741,6 +741,32 @@ h1, h2, h3, h4, h5, h6 {
margin-top: 32px;
margin-bottom: -32px; // stylelint-disable-line declaration-block-no-redundant-longhand-properties
}
+
+ // Always hide the table of content. This is usually hidden by the mf-section-0 rules,
+ // but can sometimes appear elsewhere (e.g in the lede section overlay)
+ // stylelint-disable-next-line selector-class-pattern
+ .toc {
+ display: none;
+ }
+
+ // Override Minerva rule that always hides tmbox.
+ // stylelint-disable-next-line selector-class-pattern
+ .ext-discussiontools-ui-ledeSectionDialog-content.content .tmbox {
+ // stylelint-disable-next-line declaration-no-important
+ display: block !important;
+ }
+}
+
+.ext-discussiontools-init-lede-button-container {
+ margin: 0.5em 0;
+}
+
+.ext-discussiontools-init-lede-button {
+ opacity: 0.66;
+
+ > .oo-ui-buttonElement-button {
+ font-weight: normal;
+ }
}
// HACK: Fake disabled styles for the .mw-ui-button in Vector sticky header (T307726)
diff --git a/modules/mobile.js b/modules/mobile.js
index cf4c035d8..45e2a322c 100644
--- a/modules/mobile.js
+++ b/modules/mobile.js
@@ -1,4 +1,4 @@
-var $readAsWikiPage;
+var $readAsWikiPage, ledeSectionDialog;
function init( $container ) {
// For compatibility with Minerva click tracking (T295490)
@@ -31,6 +31,24 @@ function init( $container ) {
e.stopPropagation();
} );
} );
+
+ var $ledeContent = $container.find( '.mf-section-0' ).children( ':not( .ext-discussiontools-emptystate )' );
+ var $ledeButton = $container.find( '.ext-discussiontools-init-lede-button' );
+ if ( $ledeButton.length ) {
+ var windowManager = OO.ui.getWindowManager();
+ if ( !ledeSectionDialog ) {
+ var LedeSectionDialog = require( './LedeSectionDialog.js' );
+ ledeSectionDialog = new LedeSectionDialog();
+ windowManager.addWindows( [ ledeSectionDialog ] );
+ }
+
+ // Lede section popup
+ OO.ui.infuse( $ledeButton ).on( 'click', function () {
+ mw.loader.using( 'oojs-ui-windows' ).then( function () {
+ windowManager.openWindow( 'ledeSection', { $content: $ledeContent } );
+ } );
+ } );
+ }
if ( !$readAsWikiPage ) {
// Read as wiki page button, copied from renderReadAsWikiPageButton in Minerva
$readAsWikiPage = $( '