Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
41.34% |
105 / 254 |
|
21.74% |
5 / 23 |
CRAP | |
0.00% |
0 / 1 |
Hooks | |
41.34% |
105 / 254 |
|
21.74% |
5 / 23 |
1405.43 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isVectorSkin | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getActiveABTest | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
getVectorSearchResourceLoaderConfig | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
onSkinPageReadyConfig | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
updateActionsMenu | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
30 | |||
updateViewsMenuIcons | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
updateAssociatedPagesMenuIcons | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
appendClassToItem | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
6 | |||
updateUserLinksDropdownItems | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
10.02 | |||
fixEcho | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
30 | |||
updateUserLinksItems | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
makeMenuItemCollapsible | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeIcon | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
updateItemData | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
7 | |||
updateMenuItemData | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
updateMenuItems | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
createMoreOverflowMenu | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
onSkinTemplateNavigation | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
7.23 | |||
onGetPreferences | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
6 | |||
onLocalUserCreated | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
isSkinVersionLegacy | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onGetBetaFeaturePreferences | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Skins\Vector; |
4 | |
5 | use MediaWiki\Auth\Hook\LocalUserCreatedHook; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Context\RequestContext; |
8 | use MediaWiki\MediaWikiServices; |
9 | use MediaWiki\Preferences\Hook\GetPreferencesHook; |
10 | use MediaWiki\ResourceLoader as RL; |
11 | use MediaWiki\Skins\Hook\SkinPageReadyConfigHook; |
12 | use MediaWiki\Skins\Vector\Hooks\HookRunner; |
13 | use MediaWiki\User\Options\UserOptionsManager; |
14 | use MediaWiki\User\User; |
15 | use RuntimeException; |
16 | use SkinTemplate; |
17 | |
18 | /** |
19 | * Presentation hook handlers for Vector skin. |
20 | * |
21 | * Hook handler method names should be in the form of: |
22 | * on<HookName>() |
23 | * @package Vector |
24 | * @internal |
25 | */ |
26 | class Hooks implements |
27 | GetPreferencesHook, |
28 | LocalUserCreatedHook, |
29 | SkinPageReadyConfigHook |
30 | { |
31 | private Config $config; |
32 | private UserOptionsManager $userOptionsManager; |
33 | |
34 | public function __construct( |
35 | Config $config, |
36 | UserOptionsManager $userOptionsManager |
37 | ) { |
38 | $this->config = $config; |
39 | $this->userOptionsManager = $userOptionsManager; |
40 | } |
41 | |
42 | /** |
43 | * Checks if the current skin is a variant of Vector |
44 | * |
45 | * @param string $skinName |
46 | * @return bool |
47 | */ |
48 | private static function isVectorSkin( string $skinName ): bool { |
49 | return ( |
50 | $skinName === Constants::SKIN_NAME_LEGACY || |
51 | $skinName === Constants::SKIN_NAME_MODERN |
52 | ); |
53 | } |
54 | |
55 | /** |
56 | * @param RL\Context $context |
57 | * @param Config $config |
58 | * @return array |
59 | */ |
60 | public static function getActiveABTest( |
61 | RL\Context $context, |
62 | Config $config |
63 | ) { |
64 | $ab = $config->get( |
65 | Constants::CONFIG_WEB_AB_TEST_ENROLLMENT |
66 | ); |
67 | if ( count( $ab ) === 0 ) { |
68 | // If array is empty then no experiment and need to validate. |
69 | return $ab; |
70 | } |
71 | if ( !array_key_exists( 'buckets', $ab ) ) { |
72 | throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Must contain buckets key.' ); |
73 | } |
74 | if ( !array_key_exists( 'unsampled', $ab['buckets'] ) ) { |
75 | throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Must define an `unsampled` bucket.' ); |
76 | } else { |
77 | // check bucket values. |
78 | foreach ( $ab['buckets'] as $bucketName => $bucketDefinition ) { |
79 | if ( !is_array( $bucketDefinition ) ) { |
80 | throw new RuntimeException( 'Invalid VectorWebABTestEnrollment value: Buckets should be arrays' ); |
81 | } |
82 | $samplingRate = $bucketDefinition['samplingRate']; |
83 | if ( is_string( $samplingRate ) ) { |
84 | throw new RuntimeException( |
85 | 'Invalid VectorWebABTestEnrollment value: Sampling rate should be number between 0 and 1.' |
86 | ); |
87 | } |
88 | } |
89 | } |
90 | |
91 | return $ab; |
92 | } |
93 | |
94 | /** |
95 | * Generates config variables for skins.vector.search Resource Loader module (defined in |
96 | * skin.json). |
97 | * |
98 | * @param RL\Context $context |
99 | * @param Config $config |
100 | * @return array<string,mixed> |
101 | */ |
102 | public static function getVectorSearchResourceLoaderConfig( |
103 | RL\Context $context, |
104 | Config $config |
105 | ): array { |
106 | $vectorSearchConfig = [ |
107 | 'highlightQuery' => |
108 | VectorServices::getLanguageService()->canWordsBeSplitSafely( $context->getLanguage() ) |
109 | ]; |
110 | |
111 | $hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ); |
112 | $hookRunner->onVectorSearchResourceLoaderConfig( $vectorSearchConfig ); |
113 | |
114 | return array_merge( $config->get( 'VectorWvuiSearchOptions' ), $vectorSearchConfig ); |
115 | } |
116 | |
117 | /** |
118 | * SkinPageReadyConfig hook handler |
119 | * |
120 | * Replace searchModule provided by skin. |
121 | * |
122 | * @since 1.35 |
123 | * @param RL\Context $context |
124 | * @param mixed[] &$config Associative array of configurable options |
125 | * @return void This hook must not abort, it must return no value |
126 | */ |
127 | public function onSkinPageReadyConfig( |
128 | RL\Context $context, |
129 | array &$config |
130 | ): void { |
131 | // It's better to exit before any additional check |
132 | if ( !self::isVectorSkin( $context->getSkin() ) ) { |
133 | return; |
134 | } |
135 | |
136 | // Tell the `mediawiki.page.ready` module not to wire up search. |
137 | // This allows us to use the new Vue implementation. |
138 | // Context has no knowledge of legacy / modern Vector |
139 | // and from its point of view they are the same thing. |
140 | // Please see the modules `skins.vector.js` and `skins.vector.legacy.js` |
141 | // for the wire up of search. |
142 | $config['search'] = false; |
143 | } |
144 | |
145 | /** |
146 | * Moves watch item from actions to views menu. |
147 | * |
148 | * @internal used inside Hooks::onSkinTemplateNavigation |
149 | * @param array &$content_navigation |
150 | */ |
151 | private static function updateActionsMenu( &$content_navigation ) { |
152 | $key = null; |
153 | if ( isset( $content_navigation['actions']['watch'] ) ) { |
154 | $key = 'watch'; |
155 | } |
156 | if ( isset( $content_navigation['actions']['unwatch'] ) ) { |
157 | $key = 'unwatch'; |
158 | } |
159 | |
160 | // Promote watch link from actions to views and add an icon |
161 | // The second check to isset is pointless but shuts up phan. |
162 | if ( $key !== null && isset( $content_navigation['actions'][ $key ] ) ) { |
163 | $content_navigation['views'][$key] = $content_navigation['actions'][$key]; |
164 | unset( $content_navigation['actions'][$key] ); |
165 | } |
166 | } |
167 | |
168 | /** |
169 | * Adds icons to items in the "views" menu. |
170 | * |
171 | * @internal used inside Hooks::onSkinTemplateNavigation |
172 | * @param array &$content_navigation |
173 | * @param bool $isLegacy is this the legacy Vector skin? |
174 | */ |
175 | private static function updateViewsMenuIcons( &$content_navigation, $isLegacy ) { |
176 | // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset |
177 | foreach ( $content_navigation['views'] as &$item ) { |
178 | $icon = $item['icon'] ?? null; |
179 | if ( $icon ) { |
180 | if ( $isLegacy ) { |
181 | self::appendClassToItem( |
182 | $item['class'], |
183 | [ 'icon' ] |
184 | ); |
185 | } else { |
186 | // Force the item as a button with hidden text. |
187 | $item['button'] = true; |
188 | $item['text-hidden'] = true; |
189 | $item = self::updateMenuItemData( $item, false ); |
190 | } |
191 | } elseif ( !$isLegacy ) { |
192 | // The vector-tab-noicon class is only used in Vector-22. |
193 | self::appendClassToItem( |
194 | $item['class'], |
195 | [ 'vector-tab-noicon' ] |
196 | ); |
197 | } |
198 | } |
199 | } |
200 | |
201 | /** |
202 | * All associated pages menu items do not have icons so are given the vector-tab-noicon class. |
203 | * |
204 | * @internal used inside Hooks::onSkinTemplateNavigation |
205 | * @param array &$content_navigation |
206 | */ |
207 | private static function updateAssociatedPagesMenuIcons( &$content_navigation ) { |
208 | foreach ( $content_navigation['associated-pages'] as &$item ) { |
209 | self::appendClassToItem( |
210 | $item['class'], |
211 | [ 'vector-tab-noicon' ] |
212 | ); |
213 | } |
214 | } |
215 | |
216 | /** |
217 | * Adds class to a property |
218 | * |
219 | * @param array|string &$item to update |
220 | * @param array|string $classes to add to the item |
221 | */ |
222 | private static function appendClassToItem( &$item, $classes ) { |
223 | $existingClasses = $item; |
224 | |
225 | if ( is_array( $existingClasses ) ) { |
226 | // Treat as array |
227 | $newArrayClasses = is_array( $classes ) ? $classes : [ trim( $classes ) ]; |
228 | $item = array_merge( $existingClasses, $newArrayClasses ); |
229 | } elseif ( is_string( $existingClasses ) ) { |
230 | // Treat as string |
231 | $newStrClasses = is_string( $classes ) ? trim( $classes ) : implode( ' ', $classes ); |
232 | $item .= ' ' . $newStrClasses; |
233 | } else { |
234 | // Treat as whatever $classes is |
235 | $item = $classes; |
236 | } |
237 | |
238 | if ( is_string( $item ) ) { |
239 | $item = trim( $item ); |
240 | } |
241 | } |
242 | |
243 | /** |
244 | * Updates personal navigation menu (user links) dropdown for modern Vector: |
245 | * - Adds icons |
246 | * - Makes user page and watchlist collapsible |
247 | * |
248 | * @internal used inside ::updateUserLinksItems |
249 | * @param SkinTemplate $sk |
250 | * @param array &$content_navigation |
251 | * @suppress PhanTypeInvalidDimOffset |
252 | */ |
253 | private static function updateUserLinksDropdownItems( $sk, &$content_navigation ) { |
254 | // For logged-in users in modern Vector, rearrange some links in the personal toolbar. |
255 | $user = $sk->getUser(); |
256 | if ( $user->isRegistered() ) { |
257 | // Remove user page from personal menu dropdown for logged in use |
258 | $content_navigation['user-menu']['userpage']['collapsible'] = true; |
259 | // watchlist may be disabled if $wgGroupPermissions['*']['viewmywatchlist'] = false; |
260 | // See [[phab:T299671]] |
261 | if ( isset( $content_navigation['user-menu']['watchlist'] ) ) { |
262 | $content_navigation['user-menu']['watchlist']['collapsible'] = true; |
263 | } |
264 | |
265 | // Anon editor links handled manually in new anon editor menu |
266 | $logoutMenu = []; |
267 | if ( isset( $content_navigation['user-menu']['logout'] ) ) { |
268 | $logoutMenu['logout'] = $content_navigation['user-menu']['logout']; |
269 | $logoutMenu['logout']['id'] = 'pt-logout'; |
270 | unset( $content_navigation['user-menu']['logout'] ); |
271 | } |
272 | $content_navigation['user-menu-logout'] = $logoutMenu; |
273 | |
274 | self::updateMenuItems( $content_navigation, 'user-menu' ); |
275 | self::updateMenuItems( $content_navigation, 'user-menu-logout' ); |
276 | } else { |
277 | // Remove "Not logged in" from personal menu dropdown for anon users. |
278 | unset( $content_navigation['user-menu']['anonuserpage'] ); |
279 | |
280 | // Make login and create account collapsible |
281 | if ( isset( $content_navigation['user-menu']['login'] ) ) { |
282 | $content_navigation['user-menu']['login']['collapsible'] = true; |
283 | } |
284 | if ( isset( $content_navigation['user-menu']['login-private'] ) ) { |
285 | $content_navigation['user-menu']['login-private']['collapsible'] = true; |
286 | } |
287 | if ( isset( $content_navigation['user-menu']['createaccount'] ) ) { |
288 | $content_navigation['user-menu']['createaccount']['collapsible'] = true; |
289 | } |
290 | if ( isset( $content_navigation['user-menu']['sitesupport'] ) ) { |
291 | $content_navigation['user-menu']['sitesupport']['collapsible'] = true; |
292 | } |
293 | |
294 | // Anon editor links handled manually in new anon editor menu |
295 | $anonEditorMenu = []; |
296 | if ( isset( $content_navigation['user-menu']['anoncontribs'] ) ) { |
297 | $anonEditorMenu['anoncontribs'] = $content_navigation['user-menu']['anoncontribs']; |
298 | $anonEditorMenu['anoncontribs']['id'] = 'pt-anoncontribs'; |
299 | unset( $content_navigation['user-menu']['anoncontribs'] ); |
300 | } |
301 | if ( isset( $content_navigation['user-menu']['anontalk'] ) ) { |
302 | $anonEditorMenu['anontalk'] = $content_navigation['user-menu']['anontalk']; |
303 | $anonEditorMenu['anontalk']['id'] = 'pt-anontalk'; |
304 | unset( $content_navigation['user-menu']['anontalk'] ); |
305 | } |
306 | $content_navigation['user-menu-anon-editor'] = $anonEditorMenu; |
307 | |
308 | // Only show icons for anon menu items (login and create account). |
309 | self::updateMenuItems( $content_navigation, 'user-menu' ); |
310 | } |
311 | } |
312 | |
313 | /** |
314 | * Echo has styles that control icons rendering in places we don't want them. |
315 | * This code works around T343838. |
316 | * |
317 | * @param SkinTemplate $sk |
318 | * @param array &$content_navigation |
319 | */ |
320 | private static function fixEcho( $sk, &$content_navigation ) { |
321 | if ( isset( $content_navigation['notifications'] ) ) { |
322 | foreach ( $content_navigation['notifications'] as &$item ) { |
323 | $icon = $item['icon'] ?? null; |
324 | if ( $icon ) { |
325 | $linkClass = $item['link-class'] ?? []; |
326 | $newLinkClass = [ |
327 | // Allows Echo to react to clicks |
328 | 'mw-echo-notification-badge-nojs' |
329 | ]; |
330 | if ( in_array( 'mw-echo-unseen-notifications', $linkClass ) ) { |
331 | $newLinkClass[] = 'mw-echo-unseen-notifications'; |
332 | } |
333 | $item['link-class'] = $newLinkClass; |
334 | } |
335 | } |
336 | } |
337 | } |
338 | |
339 | /** |
340 | * Updates personal navigation menu (user links) for modern Vector wherein user page, create account and login links |
341 | * are removed from the dropdown to be handled separately. In legacy Vector, the custom "user-page" bucket is |
342 | * removed to preserve existing behavior. |
343 | * |
344 | * @internal used inside Hooks::onSkinTemplateNavigation |
345 | * @param SkinTemplate $sk |
346 | * @param array &$content_navigation |
347 | */ |
348 | private static function updateUserLinksItems( $sk, &$content_navigation ) { |
349 | $skinName = $sk->getSkinName(); |
350 | if ( self::isSkinVersionLegacy( $skinName ) ) { |
351 | // Remove user page from personal toolbar since it will be inside the personal menu for logged-in |
352 | // users in legacy Vector. |
353 | unset( $content_navigation['user-page'] ); |
354 | } else { |
355 | self::fixEcho( $sk, $content_navigation ); |
356 | self::updateUserLinksDropdownItems( $sk, $content_navigation ); |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * Modifies list item to make it collapsible. |
362 | * |
363 | * @internal used in ::updateItemData and ::createMoreOverflowMenu |
364 | * @param array &$item |
365 | * @param string $prefix defaults to user-links- |
366 | */ |
367 | private static function makeMenuItemCollapsible( array &$item, string $prefix = 'user-links-' ) { |
368 | $collapseMenuItemClass = $prefix . 'collapsible-item'; |
369 | self::appendClassToItem( $item[ 'class' ], $collapseMenuItemClass ); |
370 | } |
371 | |
372 | /** |
373 | * Make an icon |
374 | * |
375 | * @internal for use inside Vector skin. |
376 | * @param string $name |
377 | * @return string of HTML |
378 | */ |
379 | private static function makeIcon( $name ) { |
380 | // Html::makeLink will pass this through rawElement |
381 | return '<span class="vector-icon mw-ui-icon-' . $name . ' mw-ui-icon-wikimedia-' . $name . '"></span>'; |
382 | } |
383 | |
384 | /** |
385 | * Update template data to include classes and html that handle buttons, icons, and collapsible items. |
386 | * |
387 | * @internal used in ::updateMenuItemData |
388 | * @param array $item data to update |
389 | * @param string $buttonClassProp property to append button classes |
390 | * @param string $iconHtmlProp property to set icon HTML |
391 | * @param bool $unsetIcon should the icon field be unset? |
392 | * @return array $item Updated data |
393 | */ |
394 | private static function updateItemData( |
395 | $item, $buttonClassProp, $iconHtmlProp, $unsetIcon = true |
396 | ) { |
397 | $hasButton = $item['button'] ?? false; |
398 | $hideText = $item['text-hidden'] ?? false; |
399 | $isCollapsible = $item['collapsible'] ?? false; |
400 | $icon = $item['icon'] ?? ''; |
401 | if ( $unsetIcon ) { |
402 | unset( $item['icon'] ); |
403 | } |
404 | unset( $item['button'] ); |
405 | unset( $item['text-hidden'] ); |
406 | unset( $item['collapsible'] ); |
407 | |
408 | if ( $isCollapsible ) { |
409 | self::makeMenuItemCollapsible( $item ); |
410 | } |
411 | if ( $hasButton ) { |
412 | // Hardcoded button classes, this should be fixed by replacing Hooks.php with VectorComponentButton.php |
413 | self::appendClassToItem( $item[ $buttonClassProp ], [ |
414 | 'cdx-button', |
415 | 'cdx-button--fake-button', |
416 | 'cdx-button--fake-button--enabled', |
417 | 'cdx-button--weight-quiet' |
418 | ] ); |
419 | } |
420 | if ( $icon ) { |
421 | if ( $hideText && $hasButton ) { |
422 | self::appendClassToItem( $item[ $buttonClassProp ], [ 'cdx-button--icon-only' ] ); |
423 | } |
424 | |
425 | $item[ $iconHtmlProp ] = self::makeIcon( $icon ); |
426 | } |
427 | return $item; |
428 | } |
429 | |
430 | /** |
431 | * Updates template data for Vector menu items. |
432 | * |
433 | * @internal used inside Hooks::updateMenuItems ::updateViewsMenuIcons and ::updateUserLinksDropdownItems |
434 | * @param array $item menu item data to update |
435 | * @param bool $unsetIcon should the icon field be unset? |
436 | * @return array $item Updated menu item data |
437 | */ |
438 | private static function updateMenuItemData( $item, $unsetIcon = true ) { |
439 | $buttonClassProp = 'link-class'; |
440 | $iconHtmlProp = 'link-html'; |
441 | return self::updateItemData( $item, $buttonClassProp, $iconHtmlProp, $unsetIcon ); |
442 | } |
443 | |
444 | /** |
445 | * Updates user interface preferences for modern Vector to upgrade icon/button menu items. |
446 | * |
447 | * @param array &$content_navigation |
448 | * @param string $menu identifier |
449 | */ |
450 | private static function updateMenuItems( &$content_navigation, $menu ) { |
451 | foreach ( $content_navigation[$menu] as &$item ) { |
452 | $item = self::updateMenuItemData( $item ); |
453 | } |
454 | } |
455 | |
456 | /** |
457 | * Vector 2022 only: |
458 | * Creates an additional menu that will be injected inside the more (cactions) |
459 | * dropdown menu. This menu is a clone of `views` and this menu will only be |
460 | * shown at low resolutions (when the `views` menu is hidden). |
461 | * |
462 | * An additional menu is used instead of adding to the existing cactions menu |
463 | * so that the emptyPortlet logic for that menu is preserved and the cactions menu |
464 | * is not shown at large resolutions when empty (e.g. all items including collapsed |
465 | * items are hidden). |
466 | * |
467 | * @param array &$content_navigation |
468 | */ |
469 | private static function createMoreOverflowMenu( &$content_navigation ) { |
470 | $clonedViews = []; |
471 | foreach ( $content_navigation['views'] ?? [] as $key => $item ) { |
472 | $newItem = $item; |
473 | self::makeMenuItemCollapsible( |
474 | $newItem, |
475 | 'vector-more-' |
476 | ); |
477 | $clonedViews['more-' . $key] = $newItem; |
478 | } |
479 | // Inject collapsible menu items ahead of existing actions. |
480 | $content_navigation['views-overflow'] = $clonedViews; |
481 | } |
482 | |
483 | /** |
484 | * Upgrades Vector's watch action to a watchstar. |
485 | * This is invoked inside SkinVector, not via skin registration, as skin hooks |
486 | * are not guaranteed to run last. |
487 | * This can possibly be revised based on the outcome of T287622. |
488 | * |
489 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/SkinTemplateNavigation |
490 | * @param SkinTemplate $sk |
491 | * @param array &$content_navigation |
492 | */ |
493 | public static function onSkinTemplateNavigation( $sk, &$content_navigation ) { |
494 | $skinName = $sk->getSkinName(); |
495 | // These changes should only happen in Vector. |
496 | if ( !$skinName || !self::isVectorSkin( $skinName ) ) { |
497 | return; |
498 | } |
499 | |
500 | $title = $sk->getRelevantTitle(); |
501 | if ( |
502 | $sk->getConfig()->get( 'VectorUseIconWatch' ) && |
503 | $title && $title->canExist() |
504 | ) { |
505 | self::updateActionsMenu( $content_navigation ); |
506 | } |
507 | |
508 | self::updateUserLinksItems( $sk, $content_navigation ); |
509 | if ( $skinName === Constants::SKIN_NAME_MODERN ) { |
510 | self::createMoreOverflowMenu( $content_navigation ); |
511 | } |
512 | |
513 | // The updating of the views menu happens /after/ the overflow menu has been created |
514 | // this avoids icons showing in the more overflow menu. |
515 | self::updateViewsMenuIcons( $content_navigation, self::isSkinVersionLegacy( $skinName ) ); |
516 | self::updateAssociatedPagesMenuIcons( $content_navigation ); |
517 | } |
518 | |
519 | /** |
520 | * Adds Vector specific user preferences that can only be accessed via API. |
521 | * |
522 | * @param User $user User whose preferences are being modified. |
523 | * @param array[] &$prefs Preferences description array, to be fed to a HTMLForm object. |
524 | */ |
525 | public function onGetPreferences( $user, &$prefs ): void { |
526 | $services = MediaWikiServices::getInstance(); |
527 | $featureManagerFactory = $services->getService( 'Vector.FeatureManagerFactory' ); |
528 | $featureManager = $featureManagerFactory->createFeatureManager( RequestContext::getMain() ); |
529 | $isNightModeEnabled = $featureManager->isFeatureEnabled( Constants::FEATURE_NIGHT_MODE ); |
530 | |
531 | $vectorPrefs = [ |
532 | Constants::PREF_KEY_LIMITED_WIDTH => [ |
533 | 'type' => 'toggle', |
534 | 'label-message' => 'vector-prefs-limited-width', |
535 | 'section' => 'rendering/skin/skin-prefs', |
536 | 'help-message' => 'vector-prefs-limited-width-help', |
537 | 'hide-if' => [ '!==', 'skin', Constants::SKIN_NAME_MODERN ], |
538 | ], |
539 | Constants::PREF_KEY_FONT_SIZE => [ |
540 | 'type' => 'select', |
541 | 'label-message' => 'vector-feature-custom-font-size-name', |
542 | 'section' => 'rendering/skin/skin-prefs', |
543 | 'options-messages' => [ |
544 | 'vector-feature-custom-font-size-0-label' => '0', |
545 | 'vector-feature-custom-font-size-1-label' => '1', |
546 | 'vector-feature-custom-font-size-2-label' => '2', |
547 | ], |
548 | 'hide-if' => [ '!==', 'skin', Constants::SKIN_NAME_MODERN ], |
549 | ], |
550 | Constants::PREF_KEY_PAGE_TOOLS_PINNED => [ |
551 | 'type' => 'api' |
552 | ], |
553 | Constants::PREF_KEY_MAIN_MENU_PINNED => [ |
554 | 'type' => 'api' |
555 | ], |
556 | Constants::PREF_KEY_TOC_PINNED => [ |
557 | 'type' => 'api' |
558 | ], |
559 | Constants::PREF_KEY_APPEARANCE_PINNED => [ |
560 | 'type' => 'api' |
561 | ], |
562 | Constants::PREF_KEY_NIGHT_MODE => [ |
563 | 'type' => $isNightModeEnabled ? 'select' : 'api', |
564 | 'label-message' => 'skin-theme-name', |
565 | 'help-message' => 'skin-theme-description', |
566 | 'section' => 'rendering/skin/skin-prefs', |
567 | 'options-messages' => [ |
568 | 'skin-theme-day-label' => 'day', |
569 | 'skin-theme-night-label' => 'night', |
570 | 'skin-theme-os-label' => 'os', |
571 | ], |
572 | 'hide-if' => [ '!==', 'skin', Constants::SKIN_NAME_MODERN ], |
573 | ], |
574 | ]; |
575 | $prefs += $vectorPrefs; |
576 | } |
577 | |
578 | /** |
579 | * Called one time when initializing a users preferences for a newly created account. |
580 | * |
581 | * @param User $user Newly created user object. |
582 | * @param bool $isAutoCreated |
583 | */ |
584 | public function onLocalUserCreated( $user, $isAutoCreated ) { |
585 | $default = $this->config->get( Constants::CONFIG_KEY_DEFAULT_SKIN_VERSION_FOR_NEW_ACCOUNTS ); |
586 | if ( $default ) { |
587 | $this->userOptionsManager->setOption( |
588 | $user, |
589 | Constants::PREF_KEY_SKIN, |
590 | $default === Constants::SKIN_VERSION_LEGACY ? |
591 | Constants::SKIN_NAME_LEGACY : Constants::SKIN_NAME_MODERN |
592 | ); |
593 | } |
594 | } |
595 | |
596 | /** |
597 | * Gets whether the current skin version is the legacy version. |
598 | * |
599 | * @param string $skinName hint that can be used to detect modern vector. |
600 | * @return bool |
601 | */ |
602 | private static function isSkinVersionLegacy( $skinName ): bool { |
603 | return $skinName === Constants::SKIN_NAME_LEGACY; |
604 | } |
605 | |
606 | /** |
607 | * Register Vector 2022 beta feature to the beta features list |
608 | * |
609 | * @param User $user User the preferences are for |
610 | * @param array &$betaFeatures |
611 | */ |
612 | public function onGetBetaFeaturePreferences( User $user, array &$betaFeatures ) { |
613 | $skinName = RequestContext::getMain()->getSkinName(); |
614 | // Only Vector 2022 is supported for beta features |
615 | if ( $skinName !== Constants::SKIN_NAME_MODERN ) { |
616 | return; |
617 | } |
618 | // Only add Vector 2022 beta feature if there is at least one beta feature present in config |
619 | $configHasBeta = false; |
620 | foreach ( Constants::VECTOR_BETA_FEATURES as $featureName ) { |
621 | if ( $this->config->has( $featureName ) && $this->config->get( $featureName )[ 'beta' ] === true ) { |
622 | $configHasBeta = true; |
623 | break; |
624 | } |
625 | } |
626 | if ( !$configHasBeta ) { |
627 | return; |
628 | } |
629 | $skinsAssetsPath = $this->config->get( 'StylePath' ); |
630 | $imagesDir = "$skinsAssetsPath/Vector/resources/images"; |
631 | $betaFeatures[ Constants::VECTOR_2022_BETA_KEY ] = [ |
632 | 'label-message' => 'vector-2022-beta-preview-label', |
633 | 'desc-message' => 'vector-2022-beta-preview-description', |
634 | 'screenshot' => [ |
635 | // follow up work to add images is required in T349321 |
636 | 'ltr' => "$imagesDir/vector-2022-beta-preview-ltr.svg", |
637 | 'rtl' => "$imagesDir/vector-2022-beta-preview-rtl.svg", |
638 | ], |
639 | 'info-link' => 'https://www.mediawiki.org/wiki/Special:MyLanguage/Reading/Web/Accessibility_for_reading', |
640 | 'discussion-link' => 'https://www.mediawiki.org/wiki/Talk:Reading/Web/Accessibility_for_reading', |
641 | ]; |
642 | } |
643 | } |