Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
30.82% |
86 / 279 |
|
9.09% |
1 / 11 |
CRAP | |
0.00% |
0 / 1 |
| PageHooks | |
30.82% |
86 / 279 |
|
9.09% |
1 / 11 |
2091.95 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| isMobile | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
| onBeforePageDisplay | |
0.00% |
0 / 84 |
|
0.00% |
0 / 1 |
462 | |||
| onOutputPageBeforeHTML | |
72.73% |
32 / 44 |
|
0.00% |
0 / 1 |
14.92 | |||
| onOutputPageParserOutput | |
55.00% |
11 / 20 |
|
0.00% |
0 / 1 |
22.03 | |||
| onGetActionName | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
4.25 | |||
| onBeforeDisplayNoArticleText | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
| getEmptyStateHtml | |
44.83% |
26 / 58 |
|
0.00% |
0 / 1 |
26.79 | |||
| onSidebarBeforeOutput | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| onSkinTemplateNavigation__Universal | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
| getNewTopicsSubscriptionButton | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
56 | |||
| 1 | <?php |
| 2 | /** |
| 3 | * DiscussionTools page hooks |
| 4 | * |
| 5 | * @file |
| 6 | * @ingroup Extensions |
| 7 | * @license MIT |
| 8 | */ |
| 9 | |
| 10 | namespace MediaWiki\Extension\DiscussionTools\Hooks; |
| 11 | |
| 12 | use MediaWiki\Actions\Hook\GetActionNameHook; |
| 13 | use MediaWiki\Context\IContextSource; |
| 14 | use MediaWiki\Context\RequestContext; |
| 15 | use MediaWiki\Extension\DiscussionTools\BatchModifyElements; |
| 16 | use MediaWiki\Extension\DiscussionTools\CommentFormatter; |
| 17 | use MediaWiki\Extension\DiscussionTools\CommentUtils; |
| 18 | use MediaWiki\Extension\DiscussionTools\SubscriptionStore; |
| 19 | use MediaWiki\Extension\VisualEditor\Hooks as VisualEditorHooks; |
| 20 | use MediaWiki\Hook\SidebarBeforeOutputHook; |
| 21 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
| 22 | use MediaWiki\Html\Html; |
| 23 | use MediaWiki\MediaWikiServices; |
| 24 | use MediaWiki\Output\Hook\BeforePageDisplayHook; |
| 25 | use MediaWiki\Output\Hook\OutputPageBeforeHTMLHook; |
| 26 | use MediaWiki\Output\Hook\OutputPageParserOutputHook; |
| 27 | use MediaWiki\Output\OutputPage; |
| 28 | use MediaWiki\Page\Article; |
| 29 | use MediaWiki\Page\Hook\BeforeDisplayNoArticleTextHook; |
| 30 | use MediaWiki\Parser\ParserOutput; |
| 31 | use MediaWiki\Registration\ExtensionRegistry; |
| 32 | use MediaWiki\Skin\Skin; |
| 33 | use MediaWiki\Skin\SkinTemplate; |
| 34 | use MediaWiki\SpecialPage\SpecialPage; |
| 35 | use MediaWiki\Title\Title; |
| 36 | use MediaWiki\User\Options\UserOptionsLookup; |
| 37 | use MediaWiki\User\UserIdentity; |
| 38 | use MediaWiki\User\UserNameUtils; |
| 39 | use OOUI\ButtonWidget; |
| 40 | |
| 41 | class PageHooks implements |
| 42 | BeforeDisplayNoArticleTextHook, |
| 43 | BeforePageDisplayHook, |
| 44 | GetActionNameHook, |
| 45 | OutputPageBeforeHTMLHook, |
| 46 | OutputPageParserOutputHook, |
| 47 | SidebarBeforeOutputHook, |
| 48 | SkinTemplateNavigation__UniversalHook |
| 49 | { |
| 50 | |
| 51 | public function __construct( |
| 52 | private readonly SubscriptionStore $subscriptionStore, |
| 53 | private readonly UserNameUtils $userNameUtils, |
| 54 | private readonly UserOptionsLookup $userOptionsLookup, |
| 55 | ) { |
| 56 | } |
| 57 | |
| 58 | private function isMobile(): bool { |
| 59 | if ( ExtensionRegistry::getInstance()->isLoaded( 'MobileFrontend' ) ) { |
| 60 | /** @var \MobileContext $mobFrontContext */ |
| 61 | $mobFrontContext = MediaWikiServices::getInstance()->getService( 'MobileFrontend.Context' ); |
| 62 | return $mobFrontContext->shouldDisplayMobileView(); |
| 63 | } |
| 64 | return false; |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * Adds DiscussionTools JS to the output. |
| 69 | * |
| 70 | * This is attached to the MediaWiki 'BeforePageDisplay' hook. |
| 71 | * |
| 72 | * @param OutputPage $output |
| 73 | * @param Skin $skin |
| 74 | * @return void This hook must not abort, it must return no value |
| 75 | */ |
| 76 | public function onBeforePageDisplay( $output, $skin ): void { |
| 77 | $user = $output->getUser(); |
| 78 | $req = $output->getRequest(); |
| 79 | $title = $output->getTitle(); |
| 80 | |
| 81 | foreach ( HookUtils::FEATURES as $feature ) { |
| 82 | // Add a CSS class for each enabled feature |
| 83 | if ( HookUtils::isFeatureEnabledForOutput( $output, $feature ) ) { |
| 84 | // The following CSS classes are generated here: |
| 85 | // * ext-discussiontools-replytool-enabled |
| 86 | // * ext-discussiontools-newtopictool-enabled |
| 87 | // * ext-discussiontools-sourcemodetoolbar-enabled |
| 88 | // * ext-discussiontools-topicsubscription-enabled |
| 89 | // * ext-discussiontools-autotopicsub-enabled |
| 90 | // * ext-discussiontools-visualenhancements-enabled |
| 91 | $output->addBodyClasses( "ext-discussiontools-$feature-enabled" ); |
| 92 | } |
| 93 | } |
| 94 | |
| 95 | $isMobile = $this->isMobile(); |
| 96 | |
| 97 | if ( $isMobile && HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) { |
| 98 | $output->addBodyClasses( 'collapsible-headings-collapsed' ); |
| 99 | } |
| 100 | |
| 101 | // Load style modules if the tools can be available for the title |
| 102 | // to selectively hide DT features, depending on the body classes added above. |
| 103 | if ( HookUtils::isAvailableForTitle( $title ) ) { |
| 104 | $output->addModuleStyles( 'ext.discussionTools.init.styles' ); |
| 105 | } |
| 106 | |
| 107 | // Load modules if any DT feature is enabled for this user |
| 108 | if ( HookUtils::isFeatureEnabledForOutput( $output ) ) { |
| 109 | $output->addModules( 'ext.discussionTools.init' ); |
| 110 | |
| 111 | $enabledVars = []; |
| 112 | foreach ( HookUtils::FEATURES as $feature ) { |
| 113 | $enabledVars[$feature] = HookUtils::isFeatureEnabledForOutput( $output, $feature ); |
| 114 | } |
| 115 | $output->addJsConfigVars( 'wgDiscussionToolsFeaturesEnabled', $enabledVars ); |
| 116 | |
| 117 | $editor = $this->userOptionsLookup->getOption( $user, 'discussiontools-editmode' ); |
| 118 | // User has no preferred editor yet |
| 119 | // If the user has a preferred editor, this will be evaluated in the client |
| 120 | if ( !$editor ) { |
| 121 | // Check which editor we would use for articles |
| 122 | // VE pref is 'visualeditor'/'wikitext'. Here we describe the mode, |
| 123 | // not the editor, so 'visual'/'source' |
| 124 | $editor = VisualEditorHooks::getPreferredEditor( $user, $req ) === 'visualeditor' ? |
| 125 | 'visual' : 'source'; |
| 126 | $output->addJsConfigVars( |
| 127 | 'wgDiscussionToolsFallbackEditMode', |
| 128 | $editor |
| 129 | ); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | // Replace the action=edit§ion=new form with the new topic tool. |
| 134 | if ( HookUtils::shouldOpenNewTopicTool( $output->getContext() ) ) { |
| 135 | $output->addJsConfigVars( 'wgDiscussionToolsStartNewTopicTool', true ); |
| 136 | |
| 137 | // For no-JS compatibility, redirect to the old new section editor if JS is unavailable. |
| 138 | // This isn't great, because the user has to load the page twice. But making a page that is |
| 139 | // both a view mode and an edit mode seems difficult, so I'm cutting some corners here. |
| 140 | // (Code below adapted from VisualEditor.) |
| 141 | $params = $output->getRequest()->getValues(); |
| 142 | $params['dtenable'] = '0'; |
| 143 | $url = wfScript() . '?' . wfArrayToCgi( $params ); |
| 144 | $escapedUrl = htmlspecialchars( $url ); |
| 145 | |
| 146 | // Redirect if the user has no JS (<noscript>) |
| 147 | $output->addHeadItem( |
| 148 | 'dt-noscript-fallback', |
| 149 | "<noscript><meta http-equiv=\"refresh\" content=\"0; url=$escapedUrl\"></noscript>" |
| 150 | ); |
| 151 | // Redirect if the user has no ResourceLoader |
| 152 | $output->addScript( Html::inlineScript( |
| 153 | "(window.NORLQ=window.NORLQ||[]).push(" . |
| 154 | "function(){" . |
| 155 | "location.href=\"$url\";" . |
| 156 | "}" . |
| 157 | ");" |
| 158 | ) ); |
| 159 | } |
| 160 | |
| 161 | if ( $isMobile ) { |
| 162 | if ( |
| 163 | $title->isTalkPage() && |
| 164 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) && ( |
| 165 | // 'DiscussionTools-ledeButton' property may be already set to true or false. |
| 166 | // Examine the other conditions only if it's unset. |
| 167 | $output->getProperty( 'DiscussionTools-ledeButton' ) ?? ( |
| 168 | // Header shown on all talk pages, see Article::showNamespaceHeader |
| 169 | !$output->getContext()->msg( 'talkpageheader' )->isDisabled() && |
| 170 | // Check if it isn't empty since it may use parser functions to only show itself on some pages |
| 171 | trim( $output->getContext()->msg( 'talkpageheader' )->text() ) !== '' |
| 172 | ) |
| 173 | ) |
| 174 | ) { |
| 175 | $output->addBodyClasses( 'ext-discussiontools-init-lede-hidden' ); |
| 176 | $output->enableOOUI(); |
| 177 | $output->prependHTML( |
| 178 | Html::rawElement( 'div', |
| 179 | [ 'class' => 'ext-discussiontools-init-lede-button-container' ], |
| 180 | ( new ButtonWidget( [ |
| 181 | 'label' => $output->getContext()->msg( 'discussiontools-ledesection-button' )->text(), |
| 182 | 'classes' => [ 'ext-discussiontools-init-lede-button' ], |
| 183 | 'framed' => false, |
| 184 | 'icon' => 'info', |
| 185 | 'infusable' => true, |
| 186 | ] ) ) |
| 187 | ) |
| 188 | ); |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | if ( $output->getSkin()->getSkinName() === 'minerva' ) { |
| 193 | if ( |
| 194 | ( $req->getRawVal( 'action' ) ?? 'view' ) === 'view' && |
| 195 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) && |
| 196 | // Only add the button if "New section" tab would be shown in a normal skin. |
| 197 | HookUtils::shouldShowNewSectionTab( $output->getContext() ) |
| 198 | ) { |
| 199 | $output->enableOOUI(); |
| 200 | // For speechBubbleAdd |
| 201 | $output->addModuleStyles( 'oojs-ui.styles.icons-alerts' ); |
| 202 | $output->addBodyClasses( 'ext-discussiontools-init-new-topic-opened' ); |
| 203 | |
| 204 | // Minerva doesn't show a new topic button. |
| 205 | $output->addHTML( Html::rawElement( 'div', |
| 206 | [ 'class' => 'ext-discussiontools-init-new-topic' ], |
| 207 | ( new ButtonWidget( [ |
| 208 | 'classes' => [ 'ext-discussiontools-init-new-topic-button' ], |
| 209 | 'href' => $title->getLinkURL( [ 'action' => 'edit', 'section' => 'new' ] ), |
| 210 | 'icon' => 'speechBubbleAdd', |
| 211 | 'label' => $output->getContext()->msg( 'skin-action-addsection' )->text(), |
| 212 | 'flags' => [ 'progressive', 'primary' ], |
| 213 | 'infusable' => true, |
| 214 | ] ) ) |
| 215 | // For compatibility with MobileWebUIActionsTracking logging (T295490) |
| 216 | ->setAttributes( [ 'data-event-name' => 'talkpage.add-topic' ] ) |
| 217 | ) ); |
| 218 | } |
| 219 | |
| 220 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { |
| 221 | $output->addModuleStyles( 'ext.discussionTools.minervaicons' ); |
| 222 | } |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * OutputPageBeforeHTML hook handler |
| 228 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageBeforeHTML |
| 229 | * |
| 230 | * @param OutputPage $output OutputPage object that corresponds to the page |
| 231 | * @param string &$text Text that will be displayed, in HTML |
| 232 | * @return bool|void This hook must not abort, it must return true or null. |
| 233 | */ |
| 234 | public function onOutputPageBeforeHTML( $output, &$text ) { |
| 235 | // ParserOutputPostCacheTransform hook would be a better place to do this, |
| 236 | // so that when the ParserOutput is used directly without using this hook, |
| 237 | // we don't leave half-baked interface elements in it (see e.g. T292345, T294168). |
| 238 | // But that hook doesn't provide parameters that we need to render correctly |
| 239 | // (including the page title, interface language, and current user). |
| 240 | |
| 241 | // This hook can be executed more than once per page view if the page content is composed from |
| 242 | // multiple sources! |
| 243 | |
| 244 | $batchModifyElements = new BatchModifyElements(); |
| 245 | |
| 246 | // Match the check in ParserHooks::transformHtml which adds the timestamp link placeholders |
| 247 | if ( HookUtils::isAvailableForTitle( $output->getTitle() ) ) { |
| 248 | CommentFormatter::postprocessTimestampLinks( $text, $batchModifyElements, $output ); |
| 249 | } |
| 250 | |
| 251 | $isMobile = $this->isMobile(); |
| 252 | $visualEnhancementsEnabled = |
| 253 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ); |
| 254 | |
| 255 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { |
| 256 | // Just enable OOUI PHP - the OOUI subscribe button isn't infused unless VISUALENHANCEMENTS are enabled |
| 257 | $output->setupOOUI(); |
| 258 | CommentFormatter::postprocessTopicSubscription( |
| 259 | $text, $batchModifyElements, $output, $this->subscriptionStore, $isMobile, $visualEnhancementsEnabled |
| 260 | ); |
| 261 | } else { |
| 262 | CommentFormatter::removeTopicSubscription( $batchModifyElements ); |
| 263 | } |
| 264 | |
| 265 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) ) { |
| 266 | $output->enableOOUI(); |
| 267 | CommentFormatter::postprocessReplyTool( |
| 268 | $text, $batchModifyElements, $output, $isMobile, $visualEnhancementsEnabled |
| 269 | ); |
| 270 | } else { |
| 271 | CommentFormatter::removeReplyTool( $batchModifyElements ); |
| 272 | } |
| 273 | |
| 274 | if ( $visualEnhancementsEnabled ) { |
| 275 | $output->enableOOUI(); |
| 276 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { |
| 277 | // Visually enhanced topic subscriptions: bell, bellOutline |
| 278 | $output->addModuleStyles( 'oojs-ui.styles.icons-alerts' ); |
| 279 | } |
| 280 | if ( |
| 281 | $isMobile || |
| 282 | CommentFormatter::isLanguageRequiringReplyIcon( $output->getLanguage() ) |
| 283 | ) { |
| 284 | // Reply button: share |
| 285 | $output->addModuleStyles( 'oojs-ui.styles.icons-content' ); |
| 286 | } |
| 287 | $output->addModuleStyles( [ |
| 288 | // Overflow menu ('ellipsis' icon) |
| 289 | 'oojs-ui.styles.icons-interactions', |
| 290 | ] ); |
| 291 | if ( $isMobile ) { |
| 292 | $output->addModuleStyles( [ |
| 293 | // Edit button in overflow menu ('edit' icon) |
| 294 | 'oojs-ui.styles.icons-editing-core', |
| 295 | ] ); |
| 296 | } |
| 297 | CommentFormatter::postprocessVisualEnhancements( $text, $batchModifyElements, $output, $isMobile ); |
| 298 | } else { |
| 299 | CommentFormatter::removeVisualEnhancements( $batchModifyElements ); |
| 300 | } |
| 301 | |
| 302 | // Optimization: Only parse and process the HTML if it seems to contain our tags (T400115) |
| 303 | if ( str_contains( $text, '<mw:dt-' ) ) { |
| 304 | $text = $batchModifyElements->apply( $text ); |
| 305 | } |
| 306 | |
| 307 | // Append empty state if the OutputPageParserOutput hook decided that we should. |
| 308 | // This depends on the order in which the hooks run. Hopefully it doesn't change. |
| 309 | if ( $output->getProperty( 'DiscussionTools-emptyStateHtml' ) ) { |
| 310 | // Insert before the last </div> tag, which should belong to <div class="mw-parser-output"> |
| 311 | $idx = strrpos( $text, '</div>' ); |
| 312 | $text = substr_replace( |
| 313 | $text, |
| 314 | $output->getProperty( 'DiscussionTools-emptyStateHtml' ), |
| 315 | $idx === false ? strlen( $text ) : $idx, |
| 316 | 0 |
| 317 | ); |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * OutputPageParserOutput hook handler |
| 323 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput |
| 324 | * |
| 325 | * @param OutputPage $output |
| 326 | * @param ParserOutput $pout ParserOutput instance being added in $output |
| 327 | * @return void This hook must not abort, it must return no value |
| 328 | */ |
| 329 | public function onOutputPageParserOutput( $output, $pout ): void { |
| 330 | // ParserOutputPostCacheTransform hook would be a better place to do this, |
| 331 | // so that when the ParserOutput is used directly without using this hook, |
| 332 | // we don't leave half-baked interface elements in it (see e.g. T292345, T294168). |
| 333 | // But that hook doesn't provide parameters that we need to render correctly |
| 334 | // (including the page title, interface language, and current user). |
| 335 | |
| 336 | // This hook can be executed more than once per page view if the page content is composed from |
| 337 | // multiple sources! |
| 338 | |
| 339 | CommentFormatter::postprocessTableOfContents( $pout, $output ); |
| 340 | |
| 341 | if ( |
| 342 | CommentFormatter::isEmptyTalkPage( $pout ) && |
| 343 | HookUtils::shouldDisplayEmptyState( $output->getContext() ) |
| 344 | ) { |
| 345 | $output->enableOOUI(); |
| 346 | // This must be appended after the content of the page, which wasn't added to OutputPage yet. |
| 347 | // Pass it to the OutputPageBeforeHTML hook, so that it may add it at the right time. |
| 348 | // This depends on the order in which the hooks run. Hopefully it doesn't change. |
| 349 | $output->setProperty( 'DiscussionTools-emptyStateHtml', |
| 350 | $this->getEmptyStateHtml( $output->getContext() ) ); |
| 351 | $output->addBodyClasses( 'ext-discussiontools-emptystate-shown' ); |
| 352 | } |
| 353 | |
| 354 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) { |
| 355 | $subtitle = CommentFormatter::postprocessVisualEnhancementsSubtitle( $pout, $output ); |
| 356 | |
| 357 | if ( $subtitle ) { |
| 358 | $output->addSubtitle( $subtitle ); |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | if ( $output->getSkin()->getSkinName() === 'minerva' ) { |
| 363 | $title = $output->getTitle(); |
| 364 | |
| 365 | if ( |
| 366 | $title->isTalkPage() && |
| 367 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) |
| 368 | ) { |
| 369 | if ( |
| 370 | CommentFormatter::hasCommentsInLedeContent( $pout ) |
| 371 | ) { |
| 372 | // If there are comments in the lede section, we can't really separate them from other lede |
| 373 | // content, so keep the whole section visible. |
| 374 | $output->setProperty( 'DiscussionTools-ledeButton', false ); |
| 375 | |
| 376 | } elseif ( |
| 377 | CommentFormatter::hasLedeContent( $pout ) && |
| 378 | $output->getProperty( 'DiscussionTools-ledeButton' ) === null |
| 379 | ) { |
| 380 | // If there is lede content and the lede button hasn't been disabled above, enable it. |
| 381 | $output->setProperty( 'DiscussionTools-ledeButton', true ); |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | } |
| 386 | |
| 387 | /** |
| 388 | * GetActionName hook handler |
| 389 | * |
| 390 | * @param IContextSource $context Request context |
| 391 | * @param string &$action Default action name, reassign to change it |
| 392 | * @return void This hook must not abort, it must return no value |
| 393 | */ |
| 394 | public function onGetActionName( IContextSource $context, string &$action ): void { |
| 395 | if ( $action === 'edit' && ( |
| 396 | HookUtils::shouldOpenNewTopicTool( $context ) || |
| 397 | HookUtils::shouldDisplayEmptyState( $context ) |
| 398 | ) ) { |
| 399 | $action = 'view'; |
| 400 | } |
| 401 | } |
| 402 | |
| 403 | /** |
| 404 | * BeforeDisplayNoArticleText hook handler |
| 405 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText |
| 406 | * |
| 407 | * @param Article $article The (empty) article |
| 408 | * @return bool|void This hook can abort |
| 409 | */ |
| 410 | public function onBeforeDisplayNoArticleText( $article ) { |
| 411 | // We want to override the empty state for articles on which we would be enabled |
| 412 | $context = $article->getContext(); |
| 413 | if ( !HookUtils::shouldDisplayEmptyState( $context ) ) { |
| 414 | // Our empty states are all about using the new topic tool, but |
| 415 | // expect to be on a talk page, so fall back if it's not |
| 416 | // available or if we're in a non-talk namespace that still has |
| 417 | // DT features enabled |
| 418 | return true; |
| 419 | } |
| 420 | |
| 421 | $output = $context->getOutput(); |
| 422 | $output->enableOOUI(); |
| 423 | $output->disableClientCache(); |
| 424 | |
| 425 | $html = $this->getEmptyStateHtml( $context ); |
| 426 | |
| 427 | $output->addHTML( |
| 428 | // This being mw-parser-output is a lie, but makes the reply controller cope much better with everything |
| 429 | Html::rawElement( 'div', [ 'class' => 'mw-parser-output noarticletext' ], $html ) |
| 430 | ); |
| 431 | $output->addBodyClasses( 'ext-discussiontools-emptystate-shown' ); |
| 432 | |
| 433 | return false; |
| 434 | } |
| 435 | |
| 436 | /** |
| 437 | * Generate HTML markup for the new topic tool's empty state, shown on talk pages that don't exist |
| 438 | * or have no topics. |
| 439 | * |
| 440 | * @return string HTML |
| 441 | */ |
| 442 | private function getEmptyStateHtml( IContextSource $context ): string { |
| 443 | $coreConfig = RequestContext::getMain()->getConfig(); |
| 444 | |
| 445 | $descParams = []; |
| 446 | $buttonMsg = 'discussiontools-emptystate-button'; |
| 447 | $title = $context->getTitle(); |
| 448 | if ( $title->inNamespace( NS_USER_TALK ) && !$title->isSubpage() ) { |
| 449 | // This is a user talk page |
| 450 | $isIP = $this->userNameUtils->isIP( $title->getText() ); |
| 451 | $isTemp = $this->userNameUtils->isTemp( $title->getText() ); |
| 452 | if ( $title->equals( $context->getUser()->getTalkPage() ) ) { |
| 453 | // This is your own user talk page |
| 454 | if ( $isIP || $isTemp ) { |
| 455 | if ( $isIP ) { |
| 456 | // You're an IP editor, so this is only *sort of* your talk page |
| 457 | $titleMsg = 'discussiontools-emptystate-title-self-anon'; |
| 458 | $descMsg = 'discussiontools-emptystate-desc-self-anon'; |
| 459 | } else { |
| 460 | // You're a temporary user, so you don't get some of the good stuff |
| 461 | $titleMsg = 'discussiontools-emptystate-title-self-temp'; |
| 462 | $descMsg = 'discussiontools-emptystate-desc-self-temp'; |
| 463 | } |
| 464 | $query = $context->getRequest()->getValues(); |
| 465 | unset( $query['title'] ); |
| 466 | $descParams = [ |
| 467 | SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ |
| 468 | 'returnto' => $context->getTitle()->getFullText(), |
| 469 | 'returntoquery' => wfArrayToCgi( $query ), |
| 470 | ] ), |
| 471 | SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ |
| 472 | 'returnto' => $context->getTitle()->getFullText(), |
| 473 | 'returntoquery' => wfArrayToCgi( $query ), |
| 474 | ] ), |
| 475 | ]; |
| 476 | } else { |
| 477 | // You're logged in, this is very much your talk page |
| 478 | $titleMsg = 'discussiontools-emptystate-title-self'; |
| 479 | $descMsg = 'discussiontools-emptystate-desc-self'; |
| 480 | } |
| 481 | $buttonMsg = false; |
| 482 | } elseif ( $isIP ) { |
| 483 | // This is an IP editor |
| 484 | $titleMsg = 'discussiontools-emptystate-title-user-anon'; |
| 485 | $descMsg = 'discussiontools-emptystate-desc-user-anon'; |
| 486 | } elseif ( $isTemp ) { |
| 487 | // This is a temporary user |
| 488 | $titleMsg = 'discussiontools-emptystate-title-user-temp'; |
| 489 | $descMsg = 'discussiontools-emptystate-desc-user-temp'; |
| 490 | } else { |
| 491 | // This is any other user |
| 492 | $titleMsg = 'discussiontools-emptystate-title-user'; |
| 493 | $descMsg = 'discussiontools-emptystate-desc-user'; |
| 494 | } |
| 495 | } else { |
| 496 | // This is any other page on which DT is enabled |
| 497 | $titleMsg = 'discussiontools-emptystate-title'; |
| 498 | $descMsg = 'discussiontools-emptystate-desc'; |
| 499 | } |
| 500 | |
| 501 | $text = |
| 502 | Html::rawElement( 'h3', [], |
| 503 | $context->msg( $titleMsg )->parse() |
| 504 | ) . |
| 505 | Html::rawElement( 'div', [ 'class' => 'plainlinks' ], |
| 506 | $context->msg( $descMsg, ...$descParams )->parseAsBlock() |
| 507 | ); |
| 508 | |
| 509 | if ( $buttonMsg ) { |
| 510 | $text .= new ButtonWidget( [ |
| 511 | 'label' => $context->msg( $buttonMsg )->text(), |
| 512 | 'href' => $title->getLocalURL( 'action=edit§ion=new' ), |
| 513 | 'flags' => [ 'primary', 'progressive' ] |
| 514 | ] ); |
| 515 | } |
| 516 | |
| 517 | $wrapped = |
| 518 | Html::rawElement( 'div', [ 'class' => 'ext-discussiontools-emptystate' ], |
| 519 | Html::rawElement( 'div', [ 'class' => 'ext-discussiontools-emptystate-text' ], $text ) . |
| 520 | Html::element( 'div', [ 'class' => 'ext-discussiontools-emptystate-logo' ] ) |
| 521 | ); |
| 522 | |
| 523 | return $wrapped; |
| 524 | } |
| 525 | |
| 526 | /** |
| 527 | * @param Skin $skin |
| 528 | * @param array &$sidebar |
| 529 | */ |
| 530 | public function onSidebarBeforeOutput( $skin, &$sidebar ): void { |
| 531 | $output = $skin->getOutput(); |
| 532 | if ( |
| 533 | $skin->getSkinName() === 'minerva' && |
| 534 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) |
| 535 | ) { |
| 536 | $button = $this->getNewTopicsSubscriptionButton( |
| 537 | $skin->getUser(), |
| 538 | $skin->getTitle(), |
| 539 | $skin->getContext() |
| 540 | ); |
| 541 | $sidebar['TOOLBOX']['t-page-subscribe'] = [ |
| 542 | 'icon' => $button['icon'], |
| 543 | 'text' => $button['label'], |
| 544 | 'href' => $button['href'], |
| 545 | ]; |
| 546 | } |
| 547 | } |
| 548 | |
| 549 | /** |
| 550 | * @param SkinTemplate $sktemplate |
| 551 | * @param array &$links |
| 552 | */ |
| 553 | public function onSkinTemplateNavigation__Universal( $sktemplate, &$links ): void { |
| 554 | $output = $sktemplate->getOutput(); |
| 555 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { |
| 556 | $button = $this->getNewTopicsSubscriptionButton( |
| 557 | $sktemplate->getUser(), |
| 558 | $sktemplate->getTitle(), |
| 559 | $sktemplate->getContext() |
| 560 | ); |
| 561 | |
| 562 | $links['actions']['dt-page-subscribe'] = [ |
| 563 | 'text' => $button['label'], |
| 564 | 'title' => $button['tooltip'], |
| 565 | 'data-mw-subscribed' => $button['isSubscribed'] ? '1' : '0', |
| 566 | 'href' => $button['href'], |
| 567 | ]; |
| 568 | |
| 569 | $output->addModules( [ 'ext.discussionTools.init' ] ); |
| 570 | } |
| 571 | |
| 572 | if ( |
| 573 | $sktemplate->getSkinName() === 'minerva' && |
| 574 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::NEWTOPICTOOL ) |
| 575 | ) { |
| 576 | // Remove duplicate add topic button from page actions (T395980) |
| 577 | unset( $links[ 'views' ][ 'addsection' ] ); |
| 578 | } |
| 579 | } |
| 580 | |
| 581 | /** |
| 582 | * Get data from a new topics subcription button |
| 583 | * |
| 584 | * @param UserIdentity $user User |
| 585 | * @param Title $title Title |
| 586 | * @param IContextSource $context Context |
| 587 | * @return array Array containing label, tooltip, icon, isSubscribed and href. |
| 588 | */ |
| 589 | private function getNewTopicsSubscriptionButton( |
| 590 | UserIdentity $user, Title $title, IContextSource $context |
| 591 | ): array { |
| 592 | $items = $this->subscriptionStore->getSubscriptionItemsForUser( |
| 593 | $user, |
| 594 | [ CommentUtils::getNewTopicsSubscriptionId( $title ) ] |
| 595 | ); |
| 596 | $subscriptionItem = count( $items ) ? $items[ 0 ] : null; |
| 597 | $isSubscribed = $subscriptionItem && !$subscriptionItem->isMuted(); |
| 598 | |
| 599 | return [ |
| 600 | 'label' => $context->msg( $isSubscribed ? |
| 601 | 'discussiontools-newtopicssubscription-button-unsubscribe-label' : |
| 602 | 'discussiontools-newtopicssubscription-button-subscribe-label' |
| 603 | )->text(), |
| 604 | 'tooltip' => $context->msg( $isSubscribed ? |
| 605 | 'discussiontools-newtopicssubscription-button-unsubscribe-tooltip' : |
| 606 | 'discussiontools-newtopicssubscription-button-subscribe-tooltip' |
| 607 | )->text(), |
| 608 | 'icon' => $isSubscribed ? 'bell' : 'bellOutline', |
| 609 | 'isSubscribed' => $isSubscribed, |
| 610 | 'href' => $title->getLinkURL( [ |
| 611 | 'action' => $isSubscribed ? 'dtunsubscribe' : 'dtsubscribe', |
| 612 | 'commentname' => CommentUtils::getNewTopicsSubscriptionId( $title ), |
| 613 | ] ), |
| 614 | ]; |
| 615 | } |
| 616 | } |