From 91af0594b59c6317e2fb7d6d22ec0ffa2308d63e Mon Sep 17 00:00:00 2001 From: David Lynch Date: Thu, 29 Jul 2021 01:12:10 -0500 Subject: [PATCH] Apply an empty-state to pages with the new topic tool enabled This includes the dtrepliedto URL functionality from I3f81e4d77faed367606e47678b8896051982359d. Bug: T274831 Bug: T274832 Bug: T277329 Change-Id: I035d04f30c8312b0cb42902d3bf940df1482ffb3 --- extension.json | 1 + i18n/en.json | 9 +++ i18n/qqq.json | 9 +++ images/emptystate.svg | 119 ++++++++++++++++++++++++++++++++ includes/Hooks/HookUtils.php | 10 +-- includes/Hooks/PageHooks.php | 115 +++++++++++++++++++++++++++--- modules/NewTopicController.js | 20 ++++-- modules/ReplyLinksController.js | 5 +- modules/controller.js | 10 +++ modules/dt.init.js | 11 +++ modules/dt.init.less | 13 ++++ 11 files changed, 299 insertions(+), 23 deletions(-) create mode 100644 images/emptystate.svg diff --git a/extension.json b/extension.json index 1c85d1454..ce5033f9e 100644 --- a/extension.json +++ b/extension.json @@ -380,6 +380,7 @@ "ParserAfterParse": "parser", "ParserAfterTidy": "parser", "ParserOptionsRegister": "parser", + "BeforeDisplayNoArticleText": "page", "BeforePageDisplay": "page", "GetActionName": "page", "OutputPageBeforeHTML": "page", diff --git a/i18n/en.json b/i18n/en.json index 8ecef575e..b6a73de0a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -5,6 +5,15 @@ "discussiontools": "Discussion tools", "discussiontools-defaultsummary-reply": "Reply", "discussiontools-desc": "Tools to enhance discussion pages.", + "discussiontools-emptystate-button": "Start a discussion", + "discussiontools-emptystate-desc": "You can use this talk page to start a discussion with others about how to improve [[{{ARTICLESPACE}}:{{PAGENAME}}|{{PAGENAME}}]]. [[{{MediaWiki:discussiontools-emptystate-link-talkpages}}|Learn more about how these pages are used]].", + "discussiontools-emptystate-desc-self": "People on {{SITENAME}} can use this page to post a public message for you and you will be notified when they do. [[{{MediaWiki:discussiontools-emptystate-link-userpage}}|Learn more about this page]].", + "discussiontools-emptystate-desc-user": "You can use this talk page to start a discussion with [[{{ARTICLESPACE}}:{{PAGENAME}}|{{PAGENAME}}]] that will be public for others to see. [[{{MediaWiki:discussiontools-emptystate-link-userpage}}|Learn more about how these pages are used]].", + "discussiontools-emptystate-link-talkpages": "mediawikiwiki:Special:MyLanguage/Help:Talk_pages", + "discussiontools-emptystate-link-userpage": "mediawikiwiki:Special:MyLanguage/Help:User_page", + "discussiontools-emptystate-title": "Start a discussion about [[{{ARTICLESPACE}}:{{PAGENAME}}|{{PAGENAME}}]]", + "discussiontools-emptystate-title-self": "Welcome to your talk page", + "discussiontools-emptystate-title-user": "Start a discussion with [[{{ARTICLESPACE}}:{{PAGENAME}}|{{PAGENAME}}]]", "discussiontools-error-comment-conflict": "Your comment could not be saved, because someone else commented at the same time as you. Please try again, or reload the page to view the latest comments.", "discussiontools-error-comment-disappeared": "Could not find the comment you're replying to on the page. It might have been deleted or moved to another page. Please reload the page and try again.", "discussiontools-error-comment-is-transcluded": "The \"{{int:discussiontools-replylink}}\" link cannot be used to reply to this comment. To reply, please use the full page editor by clicking \"$1\".", diff --git a/i18n/qqq.json b/i18n/qqq.json index b0fe722a3..d10dac08a 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -14,6 +14,15 @@ "discussiontools": "{{name}}", "discussiontools-defaultsummary-reply": "Default edit summary for a reply.\n\n'''Note that this is a noun (''a reply''), not a verb (''to reply''). Alternatively you can use a past tense verb if that's more natural in your language.'''", "discussiontools-desc": "{{desc\n| name = DiscussionTools\n| url = https://www.mediawiki.org/wiki/Extension:DiscussionTools\n}}", + "discussiontools-emptystate-button": "Label for add new topic button on empty talk pages.", + "discussiontools-emptystate-desc": "Description shown on empty talk pages of the purpose of talk pages.", + "discussiontools-emptystate-desc-self": "Description shown to the user of the purpose of their own talk page if it's empty.", + "discussiontools-emptystate-desc-user": "Description shown on empty user talk pages.", + "discussiontools-emptystate-link-talkpages": "{{notranslate}}\nLink to page describing what talk pages are.\n\nUsed in:\n* {{msg-mw|discussiontools-emptystate-desc}}.\n\nTranslate to a title where most wikis in the language you're translating to have one such help page; if they don't have one, you can use [[mw:Special:MyLanguage/Help:Talk_pages]] as target.", + "discussiontools-emptystate-link-userpage": "{{notranslate}}\nLink to page describing what user pages are.\n\nUsed in:\n* {{msg-mw|discussiontools-emptystate-desc-self}} and {{msg-mw|discussiontools-emptystate-desc-user}}.\n\nTranslate to a title where most wikis in the language you're translating to have one such help page; if they don't have one, you can use [[mw:Special:MyLanguage/Help:User_page]] as target.", + "discussiontools-emptystate-title": "Heading shown on empty talk pages", + "discussiontools-emptystate-title-self": "Heading shown on the user's own empty talk page", + "discussiontools-emptystate-title-user": "Heading shown on empty user talk pages", "discussiontools-error-comment-conflict": "Error message of a comment conflict.\n\nSimilar messages:\n* {{msg-mw|visualeditor-editconflict}}\n* {{msg-mw|twocolconflict-split-tour-dialog-message-single-column-view}}\n* {{msg-mw|discussiontools-error-comment-disappeared}}", "discussiontools-error-comment-disappeared": "Error message.", "discussiontools-error-comment-is-transcluded": "Error message. Parameter: $1 – text of the 'Edit'/'Edit source' tab", diff --git a/images/emptystate.svg b/images/emptystate.svg new file mode 100644 index 000000000..65e5bfee5 --- /dev/null +++ b/images/emptystate.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/includes/Hooks/HookUtils.php b/includes/Hooks/HookUtils.php index e04c8bf80..7c498a571 100644 --- a/includes/Hooks/HookUtils.php +++ b/includes/Hooks/HookUtils.php @@ -270,17 +270,19 @@ class HookUtils { return ( // ?title=...&action=edit§ion=new + // ?title=...&action=edit&redlink=1 // ?title=...&veaction=editsource§ion=new ( $req->getVal( 'action' ) === 'edit' || $req->getVal( 'veaction' ) === 'editsource' ) && - $req->getVal( 'section' ) === 'new' && + ( $req->getVal( 'section' ) === 'new' || ( + // a redlink for an existing page will get redirected to the regular view, so we don't want + // to let our empty state take it over + $req->getVal( 'redlink' ) === '1' && !$context->getTitle()->exists() + ) ) && // Adding a new topic with preloaded text is not supported yet (T269310) !( $req->getVal( 'editintro' ) || $req->getVal( 'preload' ) || $req->getVal( 'preloadparams' ) || $req->getVal( 'preloadtitle' ) ) && - // TODO If the page doesn't exist yet, we'll need to handle the interface differently, - // for now just don't enable the tool there - $context->getTitle()->exists() && // User has new topic tool enabled (and not using &dtenable=0) self::isFeatureEnabledForOutput( $out, self::NEWTOPICTOOL ) ); diff --git a/includes/Hooks/PageHooks.php b/includes/Hooks/PageHooks.php index 68f11c22a..dcfd079cd 100644 --- a/includes/Hooks/PageHooks.php +++ b/includes/Hooks/PageHooks.php @@ -9,6 +9,7 @@ namespace MediaWiki\Extension\DiscussionTools\Hooks; +use Article; use Html; use IContextSource; use MediaWiki\Actions\Hook\GetActionNameHook; @@ -17,11 +18,15 @@ use MediaWiki\Extension\DiscussionTools\SubscriptionStore; use MediaWiki\Hook\BeforePageDisplayHook; use MediaWiki\Hook\OutputPageBeforeHTMLHook; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\Hook\BeforeDisplayNoArticleTextHook; +use OOUI\ButtonWidget; use OutputPage; +use RequestContext; use Skin; use VisualEditorHooks; class PageHooks implements + BeforeDisplayNoArticleTextHook, BeforePageDisplayHook, GetActionNameHook, OutputPageBeforeHTMLHook @@ -47,6 +52,7 @@ class PageHooks implements */ public function onBeforePageDisplay( $output, $skin ): void { $user = $output->getUser(); + $req = $output->getRequest(); // Load style modules if the tools can be available for the title // as this means the DOM may have been modified in the parser cache. if ( HookUtils::isAvailableForTitle( $output->getTitle() ) ) { @@ -68,7 +74,6 @@ class PageHooks implements $services = MediaWikiServices::getInstance(); $optionsLookup = $services->getUserOptionsLookup(); - $req = $output->getRequest(); $editor = $optionsLookup->getOption( $user, 'discussiontools-editmode' ); // User has no preferred editor yet // If the user has a preferred editor, this will be evaluated in the client @@ -96,7 +101,12 @@ class PageHooks implements } // Replace the action=edit§ion=new form with the new topic tool. - if ( HookUtils::shouldUseNewTopicTool( $output->getContext() ) ) { + if ( + HookUtils::shouldUseNewTopicTool( $output->getContext() ) && + // unless we got here via a redlink, in which case we want to allow the empty + // state to be displayed: + $req->getVal( 'redlink' ) !== '1' + ) { $output->addJsConfigVars( 'wgDiscussionToolsStartNewTopicTool', true ); // For no-JS compatibility, redirect to the old new section editor if JS is unavailable. @@ -145,12 +155,7 @@ class PageHooks implements } } - foreach ( HookUtils::FEATURES as $feature ) { - // Add a CSS class for each enabled feature - if ( HookUtils::isFeatureEnabledForOutput( $output, $feature ) ) { - $output->addBodyClasses( "ext-discussiontools-$feature-enabled" ); - } - } + $this->addFeatureBodyClasses( $output ); if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { $text = CommentFormatter::postprocessTopicSubscription( @@ -178,4 +183,98 @@ class PageHooks implements $action = 'view'; } } + + /** + * BeforeDisplayNoArticleText hook handler + * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText + * + * @param Article $article The (empty) article + * @return bool|void This hook can abort + */ + public function onBeforeDisplayNoArticleText( $article ) { + // We want to override the empty state for articles on which we would be enabled + $title = $article->getTitle(); + $oldid = $article->getOldID(); + if ( $oldid || $title->hasSourceText() ) { + // The default display will probably be useful here, so leave it. + return true; + } + $context = $article->getContext(); + $output = $context->getOutput(); + if ( !HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) ) { + // Our empty states are all about using the new topic tool + return true; + } + $output->enableOOUI(); + $output->enableClientCache( false ); + + // OutputPageBeforeHTML won't have run, since there's no parsed text + // to display, but we need these classes or reply links won't show + // after a topic is posted. + $this->addFeatureBodyClasses( $output ); + + $coreConfig = RequestContext::getMain()->getConfig(); + $iconpath = $coreConfig->get( 'ExtensionAssetsPath' ) . '/DiscussionTools/images'; + + $dir = $context->getLanguage()->getDir(); + $lang = $context->getLanguage()->getHtmlCode(); + + $output->addHTML( + // This being mw-parser-output is a lie, but makes the reply controller cope much better with everything + Html::openElement( 'div', [ 'class' => "ext-discussiontools-emptystate mw-parser-output noarticletext" ] ) . + Html::openElement( 'div', [ 'class' => "ext-discussiontools-emptystate-text" ] ) + ); + if ( $title->equals( $output->getUser()->getTalkPage() ) ) { + $output->addHTML( + Html::rawElement( 'h3', [], $context->msg( 'discussiontools-emptystate-title-self' )->parse() ) . + Html::rawElement( 'p', [], $context->msg( 'discussiontools-emptystate-desc-self' )->parse() ) + ); + } else { + $titleMsg = $title->getNamespace() == NS_USER_TALK ? + 'discussiontools-emptystate-title-user' : + 'discussiontools-emptystate-title'; + $output->addHTML( + Html::rawElement( 'h3', [], $context->msg( $titleMsg )->parse() ) . + Html::rawElement( 'p', [], + $context->msg( + $title->getNamespace() == NS_USER_TALK ? + 'discussiontools-emptystate-desc-user' : + 'discussiontools-emptystate-desc' + )->parse() + ) . + new ButtonWidget( [ + 'label' => $context->msg( 'discussiontools-emptystate-button' )->text(), + 'href' => $title->getLocalURL( 'action=edit§ion=new' ), + 'flags' => [ 'primary', 'progressive' ] + ] ) + ); + } + $output->addHTML( + Html::closeElement( 'div' ) . + Html::element( 'img', [ + 'src' => $iconpath . '/emptystate.svg', + 'class' => "ext-discussiontools-emptystate-logo", + // This is a purely decorative element + 'alt' => "", + ] ) . + Html::closeElement( 'div' ) + ); + + return false; + } + + /** + * Helper to add feature-toggle classes to the output's body + * + * @param OutputPage $output + * @return void + */ + protected function addFeatureBodyClasses( OutputPage $output ): void { + foreach ( HookUtils::FEATURES as $feature ) { + // Add a CSS class for each enabled feature + if ( HookUtils::isFeatureEnabledForOutput( $output, $feature ) ) { + $output->addBodyClasses( "ext-discussiontools-$feature-enabled" ); + } + } + } } diff --git a/modules/NewTopicController.js b/modules/NewTopicController.js index 09733bc4b..94792e52c 100644 --- a/modules/NewTopicController.js +++ b/modules/NewTopicController.js @@ -46,6 +46,18 @@ OO.inheritClass( NewTopicController, CommentController ); NewTopicController.static.initType = 'section'; +NewTopicController.static.suppressedEditNotices = [ + // Ignored because we have a custom warning for non-logged-in users. + 'anoneditwarning', + // Ignored because it contains mostly instructions for signing comments using tildes. + // (Does not appear in VE notices right now, but just in case.) + 'talkpagetext', + // Ignored because the empty state takeover has already explained + // that this is a new article. + 'newarticletext', + 'newarticletextanon' +]; + /* Methods */ /** @@ -71,13 +83,7 @@ NewTopicController.prototype.setupReplyWidget = function ( replyWidget, data ) { this.$notices.empty(); for ( var noticeName in this.replyWidget.commentDetails.notices ) { - if ( - // Ignored because we have a custom warning for non-logged-in users. - noticeName === 'anoneditwarning' || - // Ignored because it contains mostly instructions for signing comments using tildes. - // (Does not appear in VE notices right now, but just in case.) - noticeName === 'talkpagetext' - ) { + if ( this.constructor.static.suppressedEditNotices.indexOf( noticeName ) !== -1 ) { continue; } var noticeItem = this.replyWidget.commentDetails.notices[ noticeName ]; diff --git a/modules/ReplyLinksController.js b/modules/ReplyLinksController.js index 9ea207d71..b412d425f 100644 --- a/modules/ReplyLinksController.js +++ b/modules/ReplyLinksController.js @@ -22,10 +22,7 @@ function ReplyLinksController( $pageContainer ) { if ( featuresEnabled.newtopictool && mw.user.options.get( 'discussiontools-newtopictool' ) ) { // eslint-disable-next-line no-jquery/no-global-selector var $addSectionTab = $( '#ca-addsection' ); - // TODO If the page doesn't exist yet, we'll need to handle the interface differently, - // for now just don't enable the tool there - var pageExists = !!mw.config.get( 'wgRelevantArticleId' ); - if ( $addSectionTab.length && pageExists ) { + if ( $addSectionTab.length ) { this.$addSectionLink = $addSectionTab.find( 'a' ); this.$addSectionLink.on( 'click keypress', this.onAddSectionLinkClickHandler ); diff --git a/modules/controller.js b/modules/controller.js index 356f68aaf..f2c336dc1 100644 --- a/modules/controller.js +++ b/modules/controller.js @@ -562,6 +562,16 @@ function update( data, comment, pageName, replyWidget ) { linksController = null; // TODO: Tell controller to teardown all other open widgets + 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. + window.location = mw.util.getUrl( pageName, { dtrepliedto: comment.id } ); + return; + } + // Update page state if ( pageName === mw.config.get( 'wgRelevantPageName' ) ) { // We can use the result from the VisualEditor API diff --git a/modules/dt.init.js b/modules/dt.init.js index 8a0c788e0..3adf7b6bf 100644 --- a/modules/dt.init.js +++ b/modules/dt.init.js @@ -9,6 +9,17 @@ var controller = require( './controller.js' ), mw.dt = {}; mw.dt.initState = {}; + +if ( uri.query.dtrepliedto ) { + // If we had to reload the page to highlight the new comment, extract that data from the URL and + // clean it up. + mw.dt.initState.repliedTo = uri.query.dtrepliedto; + if ( window.history.replaceState ) { + delete uri.query.dtrepliedto; + window.history.replaceState( {}, '', uri.toString() ); + } +} + mw.dt.init = function ( $container ) { if ( $container.is( '#mw-content-text' ) || $container.find( '#mw-content-text' ).length ) { // eslint-disable-next-line no-jquery/no-global-selector diff --git a/modules/dt.init.less b/modules/dt.init.less index 20232f078..0fc939a33 100644 --- a/modules/dt.init.less +++ b/modules/dt.init.less @@ -163,3 +163,16 @@ span[ data-mw-comment-start ] { } } } + +.ext-discussiontools-emptystate { + display: flex; + justify-content: space-between; + + .ext-discussiontools-emptystate-text > p { + margin: 2em 0; + } +} + +.ext-discussiontools-init-replylink-open .ext-discussiontools-emptystate { + display: none; +}