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