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