Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 272 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
PageHooks | |
0.00% |
0 / 272 |
|
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 / 23 |
|
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 Html; |
15 | use IContextSource; |
16 | use MediaWiki\Actions\Hook\GetActionNameHook; |
17 | use MediaWiki\Extension\DiscussionTools\CommentFormatter; |
18 | use MediaWiki\Extension\DiscussionTools\CommentUtils; |
19 | use MediaWiki\Extension\DiscussionTools\SubscriptionStore; |
20 | use MediaWiki\Extension\VisualEditor\Hooks as VisualEditorHooks; |
21 | use MediaWiki\Hook\BeforePageDisplayHook; |
22 | use MediaWiki\Hook\OutputPageBeforeHTMLHook; |
23 | use MediaWiki\Hook\OutputPageParserOutputHook; |
24 | use MediaWiki\Hook\SidebarBeforeOutputHook; |
25 | use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook; |
26 | use MediaWiki\MediaWikiServices; |
27 | use MediaWiki\Page\Hook\BeforeDisplayNoArticleTextHook; |
28 | use MediaWiki\Title\Title; |
29 | use MediaWiki\User\Options\UserOptionsLookup; |
30 | use MediaWiki\User\UserIdentity; |
31 | use MediaWiki\User\UserNameUtils; |
32 | use OOUI\ButtonWidget; |
33 | use OutputPage; |
34 | use ParserOutput; |
35 | use RequestContext; |
36 | use Skin; |
37 | use SkinTemplate; |
38 | use SpecialPage; |
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 | |
272 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) ) { |
273 | // Just enable OOUI PHP - the OOUI subscribe button isn't infused unless VISUALENHANCEMENTS are enabled |
274 | $output->setupOOUI(); |
275 | $text = CommentFormatter::postprocessTopicSubscription( |
276 | $text, $output, $this->subscriptionStore, $isMobile |
277 | ); |
278 | } |
279 | |
280 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) ) { |
281 | $output->enableOOUI(); |
282 | $text = CommentFormatter::postprocessReplyTool( $text, $output, $isMobile ); |
283 | } |
284 | |
285 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) { |
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 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS_REPLY ) && |
295 | CommentFormatter::isLanguageRequiringReplyIcon( $output->getLanguage() ) |
296 | ) |
297 | ) { |
298 | // Reply button: share |
299 | $output->addModuleStyles( 'oojs-ui.styles.icons-content' ); |
300 | } |
301 | if ( $isMobile ) { |
302 | $output->addModuleStyles( [ |
303 | // Mobile overflow menu: |
304 | // ellipsis |
305 | 'oojs-ui.styles.icons-interactions', |
306 | // edit |
307 | 'oojs-ui.styles.icons-editing-core', |
308 | ] ); |
309 | } |
310 | $text = CommentFormatter::postprocessVisualEnhancements( $text, $output, $isMobile ); |
311 | } |
312 | } |
313 | |
314 | /** |
315 | * OutputPageParserOutput hook handler |
316 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OutputPageParserOutput |
317 | * |
318 | * @param OutputPage $output |
319 | * @param ParserOutput $pout ParserOutput instance being added in $output |
320 | * @return void This hook must not abort, it must return no value |
321 | */ |
322 | public function onOutputPageParserOutput( $output, $pout ): void { |
323 | // ParserOutputPostCacheTransform hook would be a better place to do this, |
324 | // so that when the ParserOutput is used directly without using this hook, |
325 | // we don't leave half-baked interface elements in it (see e.g. T292345, T294168). |
326 | // But that hook doesn't provide parameters that we need to render correctly |
327 | // (including the page title, interface language, and current user). |
328 | |
329 | // This hook can be executed more than once per page view if the page content is composed from |
330 | // multiple sources! |
331 | |
332 | CommentFormatter::postprocessTableOfContents( $pout, $output ); |
333 | |
334 | if ( |
335 | CommentFormatter::isEmptyTalkPage( $pout ) && |
336 | HookUtils::shouldDisplayEmptyState( $output->getContext() ) |
337 | ) { |
338 | $output->enableOOUI(); |
339 | CommentFormatter::appendToEmptyTalkPage( |
340 | $pout, $this->getEmptyStateHtml( $output->getContext() ) |
341 | ); |
342 | $output->addBodyClasses( 'ext-discussiontools-emptystate-shown' ); |
343 | } |
344 | |
345 | if ( HookUtils::isFeatureEnabledForOutput( $output, HookUtils::VISUALENHANCEMENTS ) ) { |
346 | $subtitle = CommentFormatter::postprocessVisualEnhancementsSubtitle( $pout, $output ); |
347 | |
348 | if ( $subtitle ) { |
349 | $output->addSubtitle( $subtitle ); |
350 | } |
351 | } |
352 | |
353 | if ( $output->getSkin()->getSkinName() === 'minerva' ) { |
354 | $title = $output->getTitle(); |
355 | |
356 | if ( |
357 | $title->isTalkPage() && |
358 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::REPLYTOOL ) |
359 | ) { |
360 | if ( |
361 | CommentFormatter::hasCommentsInLedeContent( $pout ) |
362 | ) { |
363 | // If there are comments in the lede section, we can't really separate them from other lede |
364 | // content, so keep the whole section visible. |
365 | $output->setProperty( 'DiscussionTools-ledeButton', false ); |
366 | |
367 | } elseif ( |
368 | CommentFormatter::hasLedeContent( $pout ) && |
369 | $output->getProperty( 'DiscussionTools-ledeButton' ) === null |
370 | ) { |
371 | // If there is lede content and the lede button hasn't been disabled above, enable it. |
372 | $output->setProperty( 'DiscussionTools-ledeButton', true ); |
373 | } |
374 | } |
375 | } |
376 | } |
377 | |
378 | /** |
379 | * GetActionName hook handler |
380 | * |
381 | * @param IContextSource $context Request context |
382 | * @param string &$action Default action name, reassign to change it |
383 | * @return void This hook must not abort, it must return no value |
384 | */ |
385 | public function onGetActionName( IContextSource $context, string &$action ): void { |
386 | if ( $action === 'edit' && ( |
387 | HookUtils::shouldOpenNewTopicTool( $context ) || |
388 | HookUtils::shouldDisplayEmptyState( $context ) |
389 | ) ) { |
390 | $action = 'view'; |
391 | } |
392 | } |
393 | |
394 | /** |
395 | * BeforeDisplayNoArticleText hook handler |
396 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/BeforeDisplayNoArticleText |
397 | * |
398 | * @param Article $article The (empty) article |
399 | * @return bool|void This hook can abort |
400 | */ |
401 | public function onBeforeDisplayNoArticleText( $article ) { |
402 | // We want to override the empty state for articles on which we would be enabled |
403 | $context = $article->getContext(); |
404 | if ( !HookUtils::shouldDisplayEmptyState( $context ) ) { |
405 | // Our empty states are all about using the new topic tool, but |
406 | // expect to be on a talk page, so fall back if it's not |
407 | // available or if we're in a non-talk namespace that still has |
408 | // DT features enabled |
409 | return true; |
410 | } |
411 | |
412 | $output = $context->getOutput(); |
413 | $output->enableOOUI(); |
414 | $output->disableClientCache(); |
415 | |
416 | $html = $this->getEmptyStateHtml( $context ); |
417 | |
418 | $output->addHTML( |
419 | // This being mw-parser-output is a lie, but makes the reply controller cope much better with everything |
420 | Html::rawElement( 'div', [ 'class' => 'mw-parser-output noarticletext' ], $html ) |
421 | ); |
422 | $output->addBodyClasses( 'ext-discussiontools-emptystate-shown' ); |
423 | |
424 | return false; |
425 | } |
426 | |
427 | /** |
428 | * Generate HTML markup for the new topic tool's empty state, shown on talk pages that don't exist |
429 | * or have no topics. |
430 | * |
431 | * @param IContextSource $context |
432 | * @return string HTML |
433 | */ |
434 | private function getEmptyStateHtml( IContextSource $context ): string { |
435 | $coreConfig = RequestContext::getMain()->getConfig(); |
436 | $iconpath = $coreConfig->get( 'ExtensionAssetsPath' ) . '/DiscussionTools/images'; |
437 | |
438 | $descParams = []; |
439 | $buttonMsg = 'discussiontools-emptystate-button'; |
440 | $title = $context->getTitle(); |
441 | if ( $title->getNamespace() === NS_USER_TALK && !$title->isSubpage() ) { |
442 | // This is a user talk page |
443 | $isIP = $this->userNameUtils->isIP( $title->getText() ); |
444 | $isTemp = $this->userNameUtils->isTemp( $title->getText() ); |
445 | if ( $title->equals( $context->getUser()->getTalkPage() ) ) { |
446 | // This is your own user talk page |
447 | if ( $isIP || $isTemp ) { |
448 | if ( $isIP ) { |
449 | // You're an IP editor, so this is only *sort of* your talk page |
450 | $titleMsg = 'discussiontools-emptystate-title-self-anon'; |
451 | $descMsg = 'discussiontools-emptystate-desc-self-anon'; |
452 | } else { |
453 | // You're a temporary user, so you don't get some of the good stuff |
454 | $titleMsg = 'discussiontools-emptystate-title-self-temp'; |
455 | $descMsg = 'discussiontools-emptystate-desc-self-temp'; |
456 | } |
457 | $query = $context->getRequest()->getValues(); |
458 | unset( $query['title'] ); |
459 | $descParams = [ |
460 | SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [ |
461 | 'returnto' => $context->getTitle()->getFullText(), |
462 | 'returntoquery' => wfArrayToCgi( $query ), |
463 | ] ), |
464 | SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [ |
465 | 'returnto' => $context->getTitle()->getFullText(), |
466 | 'returntoquery' => wfArrayToCgi( $query ), |
467 | ] ), |
468 | ]; |
469 | } else { |
470 | // You're logged in, this is very much your talk page |
471 | $titleMsg = 'discussiontools-emptystate-title-self'; |
472 | $descMsg = 'discussiontools-emptystate-desc-self'; |
473 | } |
474 | $buttonMsg = false; |
475 | } elseif ( $isIP ) { |
476 | // This is an IP editor |
477 | $titleMsg = 'discussiontools-emptystate-title-user-anon'; |
478 | $descMsg = 'discussiontools-emptystate-desc-user-anon'; |
479 | } elseif ( $isTemp ) { |
480 | // This is a temporary user |
481 | $titleMsg = 'discussiontools-emptystate-title-user-temp'; |
482 | $descMsg = 'discussiontools-emptystate-desc-user-temp'; |
483 | } else { |
484 | // This is any other user |
485 | $titleMsg = 'discussiontools-emptystate-title-user'; |
486 | $descMsg = 'discussiontools-emptystate-desc-user'; |
487 | } |
488 | } else { |
489 | // This is any other page on which DT is enabled |
490 | $titleMsg = 'discussiontools-emptystate-title'; |
491 | $descMsg = 'discussiontools-emptystate-desc'; |
492 | } |
493 | |
494 | $text = |
495 | Html::rawElement( 'h3', [], |
496 | $context->msg( $titleMsg )->parse() |
497 | ) . |
498 | Html::rawElement( 'div', [ 'class' => 'plainlinks' ], |
499 | $context->msg( $descMsg, $descParams )->parseAsBlock() |
500 | ); |
501 | |
502 | if ( $buttonMsg ) { |
503 | $text .= new ButtonWidget( [ |
504 | 'label' => $context->msg( $buttonMsg )->text(), |
505 | 'href' => $title->getLocalURL( 'action=edit§ion=new' ), |
506 | 'flags' => [ 'primary', 'progressive' ] |
507 | ] ); |
508 | } |
509 | |
510 | $wrapped = |
511 | Html::rawElement( 'div', [ 'class' => 'ext-discussiontools-emptystate' ], |
512 | Html::rawElement( 'div', [ 'class' => 'ext-discussiontools-emptystate-text' ], $text ) . |
513 | Html::element( 'img', [ |
514 | 'src' => $iconpath . '/emptystate.svg', |
515 | 'class' => 'ext-discussiontools-emptystate-logo', |
516 | // This is a purely decorative element |
517 | 'alt' => '', |
518 | ] ) |
519 | ); |
520 | |
521 | return $wrapped; |
522 | } |
523 | |
524 | /** |
525 | * @param Skin $skin |
526 | * @param array &$sidebar |
527 | */ |
528 | public function onSidebarBeforeOutput( $skin, &$sidebar ): void { |
529 | $output = $skin->getOutput(); |
530 | if ( |
531 | $skin->getSkinName() === 'minerva' && |
532 | HookUtils::isFeatureEnabledForOutput( $output, HookUtils::TOPICSUBSCRIPTION ) |
533 | ) { |
534 | $button = $this->getNewTopicsSubscriptionButton( |
535 | $skin->getUser(), |
536 | $skin->getTitle(), |
537 | $skin->getContext() |
538 | ); |
539 | $sidebar['TOOLBOX']['t-page-subscribe'] = [ |
540 | 'icon' => $button['icon'], |
541 | 'text' => $button['label'], |
542 | 'href' => $button['href'], |
543 | ]; |
544 | } |
545 | } |
546 | |
547 | /** |
548 | * @param SkinTemplate $sktemplate |
549 | * @param array &$links |
550 | * @return void |
551 | * @phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName |
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 | |
573 | /** |
574 | * Get data from a new topics subcription button |
575 | * |
576 | * @param UserIdentity $user User |
577 | * @param Title $title Title |
578 | * @param IContextSource $context Context |
579 | * @return array Array containing label, tooltip, icon, isSubscribed and href. |
580 | */ |
581 | private function getNewTopicsSubscriptionButton( |
582 | UserIdentity $user, Title $title, IContextSource $context |
583 | ): array { |
584 | $items = $this->subscriptionStore->getSubscriptionItemsForUser( |
585 | $user, |
586 | [ CommentUtils::getNewTopicsSubscriptionId( $title ) ] |
587 | ); |
588 | $subscriptionItem = count( $items ) ? $items[ 0 ] : null; |
589 | $isSubscribed = $subscriptionItem && !$subscriptionItem->isMuted(); |
590 | |
591 | return [ |
592 | 'label' => $context->msg( $isSubscribed ? |
593 | 'discussiontools-newtopicssubscription-button-unsubscribe-label' : |
594 | 'discussiontools-newtopicssubscription-button-subscribe-label' |
595 | )->text(), |
596 | 'tooltip' => $context->msg( $isSubscribed ? |
597 | 'discussiontools-newtopicssubscription-button-unsubscribe-tooltip' : |
598 | 'discussiontools-newtopicssubscription-button-subscribe-tooltip' |
599 | )->text(), |
600 | 'icon' => $isSubscribed ? 'bell' : 'bellOutline', |
601 | 'isSubscribed' => $isSubscribed, |
602 | 'href' => $title->getLinkURL( [ |
603 | 'action' => $isSubscribed ? 'dtunsubscribe' : 'dtsubscribe', |
604 | 'commentname' => CommentUtils::getNewTopicsSubscriptionId( $title ), |
605 | ] ), |
606 | ]; |
607 | } |
608 | } |