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