diff --git a/i18n/en.json b/i18n/en.json index 50bb3a8..884c89e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -47,6 +47,7 @@ "mobile-frontend-user-page-member-since": "{{GENDER:$2|Joined}} $1", "mobile-frontend-user-page-talk": "Talk", "mobile-frontend-user-page-uploads": "Uploads", + "minerva-user-menu-button": "User menu", "minerva-page-actions-overflow": "Secondary page actions submenu", "minerva-page-actions-info": "Page information", "minerva-page-actions-permalink": "Permanent link", diff --git a/i18n/qqq.json b/i18n/qqq.json index 7d24053..012ced5 100644 --- a/i18n/qqq.json +++ b/i18n/qqq.json @@ -56,6 +56,7 @@ "mobile-frontend-user-page-member-since": "Message below the heading. $1 is the user registration date. $2 is the gender associated with the user account.", "mobile-frontend-user-page-talk": "Text of the link to the user's talk page\n{{Identical|Talk}}", "mobile-frontend-user-page-uploads": "Text of the link to the user's uploads page\n{{Identical|Upload}}", + "minerva-user-menu-button": "Text describing the user menu", "minerva-page-actions-overflow": "Text describing the secondary page menu button's action", "minerva-page-actions-info": "In the secondary page menu, the page information button label", "minerva-page-actions-permalink": "In the secondary page menu, the permanent link button label", diff --git a/includes/menu/Definitions.php b/includes/menu/Definitions.php index 8ccc1c0..2977931 100644 --- a/includes/menu/Definitions.php +++ b/includes/menu/Definitions.php @@ -24,6 +24,8 @@ use IContextSource; use MediaWiki\Special\SpecialPageFactory; use MediaWiki\Minerva\Menu\Entries\AuthMenuEntry; use MediaWiki\Minerva\Menu\Entries\HomeMenuEntry; +use MediaWiki\Minerva\Menu\Entries\LogInMenuEntry; +use MediaWiki\Minerva\Menu\Entries\LogOutMenuEntry; use MediaWiki\Minerva\Menu\Entries\SingleMenuEntry; use Message; use MinervaUI; @@ -108,6 +110,32 @@ final class Definitions { ) ); } + /** + * Creates a log in or log out button. + * + * @param Group $group + * @throws MWException + */ + public function insertLogInMenuItem( Group $group ) { + $group->insertEntry( new LogInMenuEntry( + $this->context, + $this->newLogInOutQuery( $this->newReturnToQuery() ) + ) ); + } + + /** + * Creates a log in or log out button. + * + * @param Group $group + * @throws MWException + */ + public function insertLogOutMenuItem( Group $group ) { + $group->insertEntry( new LogOutMenuEntry( + $this->context, + $this->newLogInOutQuery( $this->newReturnToQuery() ) + ) ); + } + /** * Creates a login or logout button with a profile button. * diff --git a/includes/menu/Main/AdvancedBuilder.php b/includes/menu/Main/AdvancedBuilder.php index e455910..2bd0539 100644 --- a/includes/menu/Main/AdvancedBuilder.php +++ b/includes/menu/Main/AdvancedBuilder.php @@ -97,13 +97,6 @@ final class AdvancedBuilder implements IBuilder { private function getPersonalTools(): Group { $group = new Group(); - $this->definitions->insertAuthMenuItem( $group ); - - if ( $this->user->isLoggedIn() ) { - $this->definitions->insertWatchlistMenuItem( $group ); - $this->definitions->insertContributionsMenuItem( $group ); - } - // Allow other extensions to add or override tools Hooks::run( 'MobileMenu', [ 'personal', &$group ] ); return $group; diff --git a/includes/menu/PageActions/UserNamespaceOverflowBuilder.php b/includes/menu/PageActions/UserNamespaceOverflowBuilder.php index 286c80d..4edd291 100644 --- a/includes/menu/PageActions/UserNamespaceOverflowBuilder.php +++ b/includes/menu/PageActions/UserNamespaceOverflowBuilder.php @@ -121,7 +121,7 @@ class UserNamespaceOverflowBuilder implements IOverflowBuilder { * Build entry based on the $toolbox element * * @param string $name - * @param string $icon Wikimedia UI icon name. + * @param string $icon Icon CSS class name. * @param string $toolboxIdx * @param array $toolbox An array of common toolbox items from the sidebar menu * @return PageActionMenuEntry|null diff --git a/includes/menu/User/AdvancedUserMenuBuilder.php b/includes/menu/User/AdvancedUserMenuBuilder.php new file mode 100644 index 0000000..4b985be --- /dev/null +++ b/includes/menu/User/AdvancedUserMenuBuilder.php @@ -0,0 +1,100 @@ +context = $context; + $this->user = $user; + $this->definitions = $definitions; + $this->sandbox = $sandbox; + } + + /** + * @inheritDoc + * @return Group + */ + public function getGroup(): Group { + $group = new Group(); + $group->insertEntry( new ProfileMenuEntry( $this->user ) ); + $group->insertEntry( new SingleMenuEntry( + 'userTalk', + $this->context->msg( 'mobile-frontend-user-page-talk' )->escaped(), + $this->user->getUserPage()->getTalkPage()->getLocalURL(), + true, + null, + 'before', + 'wikimedia-ui-userTalk-base20' + ) ); + if ( $this->sandbox ) { + $group->insertEntry( new SingleMenuEntry( + 'userSandbox', + $this->sandbox['text'], + $this->sandbox['href'] + ) ); + } + $this->definitions->insertWatchlistMenuItem( $group ); + $this->definitions->insertContributionsMenuItem( $group ); + if ( $this->user->isAnon() ) { + $this->definitions->insertLogInMenuItem( $group ); + } else { + $this->definitions->insertLogOutMenuItem( $group ); + } + return $group; + } +} diff --git a/includes/menu/User/DefaultUserMenuBuilder.php b/includes/menu/User/DefaultUserMenuBuilder.php new file mode 100644 index 0000000..afe687a --- /dev/null +++ b/includes/menu/User/DefaultUserMenuBuilder.php @@ -0,0 +1,35 @@ +builder = $builder; + $this->localizer = $localizer; + } + + /** + * Build the menu data array that can be passed to views/javascript + * @return string|null + */ + public function renderMenuData() { + $entries = $this->builder->getGroup()->getEntries(); + + foreach ( $entries as &$entry ) { + foreach ( $entry['components'] as &$component ) { + $component['class'] .= ' toggle-list-item__anchor--menu'; + } + } + + $templateParser = new TemplateParser( __DIR__ . '/../../../components' ); + return empty( $entries ) + ? null + : $templateParser->processTemplate( 'ToggleList', [ + 'class' => 'minerva-user-menu', + 'checkboxID' => 'minerva-user-menu-checkbox', + 'toggleID' => 'minerva-user-menu-toggle', // See minerva.mustache too. + 'toggleClass' => MinervaUI::iconClass( + 'page-actions-overflow', 'element', 'wikimedia-ui-' . 'userAvatar' . '-base20' + ), + 'listClass' => 'minerva-user-menu-list toggle-list__list--drop-down', // See ToggleList/*.less. + 'text' => $this->localizer->msg( 'minerva-user-menu-button' )->escaped(), + 'items' => $entries + ] ); + } +} diff --git a/includes/skins/MinervaTemplate.php b/includes/skins/MinervaTemplate.php index 8d6315e..f6e6090 100644 --- a/includes/skins/MinervaTemplate.php +++ b/includes/skins/MinervaTemplate.php @@ -266,6 +266,7 @@ class MinervaTemplate extends BaseTemplate { 'postheadinghtml' => $data['postheadinghtml'] ?? '', 'haspageactions' => $hasPageActions, 'pageactionshtml' => $hasPageActions ? $this->getPageActionsHtml() : '', + 'userMenuHTML' => $data['userMenuHTML'], 'subtitle' => $data['subtitle'], 'contenthtml' => $this->getContentHtml( $data ), 'secondaryactionshtml' => $this->getSecondaryActionsHtml(), diff --git a/includes/skins/SkinMinerva.php b/includes/skins/SkinMinerva.php index 22e931f..45afae0 100644 --- a/includes/skins/SkinMinerva.php +++ b/includes/skins/SkinMinerva.php @@ -23,6 +23,10 @@ use MediaWiki\Minerva\Menu\Main\Director as MainMenuDirector; use MediaWiki\Minerva\Permissions\IMinervaPagePermissions; use MediaWiki\Minerva\SkinOptions; use MediaWiki\Minerva\SkinUserPageHelper; +use MediaWiki\Minerva\Menu\User\UserMenuDirector; +use MediaWiki\Minerva\Menu\User\AdvancedUserMenuBuilder; +use MediaWiki\Minerva\Menu\User\DefaultUserMenuBuilder; +use MediaWiki\Minerva\Menu\Definitions; /** * Minerva: Born from the godhead of Jupiter with weapons! @@ -89,6 +93,29 @@ class SkinMinerva extends SkinTemplate { return $this->mainMenu; } + /** + * @param BaseTemplate $tpl + * @return string|null + */ + private function getUserMenuHTML( BaseTemplate $tpl ) { + $services = MediaWikiServices::getInstance(); + $options = $services->getService( 'Minerva.SkinOptions' ); + $context = RequestContext::getMain(); + $definitions = new Definitions( $context, $services->getSpecialPageFactory() ); + $director = new UserMenuDirector( + $options->get( SkinOptions::OPTION_AMC ) + ? new AdvancedUserMenuBuilder( + $context, + $this->getUser(), + $definitions, + $tpl->getPersonalTools()['sandbox']['links'][0] ?? null + ) + : new DefaultUserMenuBuilder(), + $this + ); + return $director->renderMenuData(); + } + /** * Returns the site name for the footer, either as a text or tag * @return string @@ -156,6 +183,9 @@ class SkinMinerva extends SkinTemplate { // Set the links for the main menu $tpl->set( 'mainMenu', $this->getMainMenu()->getMenuData() ); + // Set the links for page secondary actions + $tpl->set( 'userMenuHTML', $this->getUserMenuHTML( $tpl ) ); + // Set the links for page secondary actions $tpl->set( 'secondary_actions', $this->getSecondaryActions( $tpl ) ); @@ -774,14 +804,6 @@ class SkinMinerva extends SkinTemplate { return []; } - /** - * Minerva skin do use any of those, there is no need to calculate that - * @return array - */ - protected function buildPersonalUrls() { - return []; - } - /** * Returns array of config variables that should be added only to this skin * for use in JavaScript. diff --git a/includes/skins/minerva.mustache b/includes/skins/minerva.mustache index 5eff74b..655ad24 100644 --- a/includes/skins/minerva.mustache +++ b/includes/skins/minerva.mustache @@ -18,8 +18,15 @@ autocomplete="off" placeholder="{{placeholder}}" aria-label="{{placeholder}}" value="{{search}}"> -
{{{searchButton}}}
- {{^isAnon}}
{{#userNotificationsData}}{{>userNotifications}}{{/userNotificationsData}}
{{/isAnon}} +
@@ -56,4 +63,4 @@
- + diff --git a/minerva.less/minerva.variables.less b/minerva.less/minerva.variables.less index fa418a5..c15c03c 100644 --- a/minerva.less/minerva.variables.less +++ b/minerva.less/minerva.variables.less @@ -34,6 +34,7 @@ @menuWidthTablet: 600px; @rightDrawerWidth: 60%; @primaryNavBackgroundColor: @colorGray14; +@menuButtonIconSize: 3.5em; // Headings @firstHeadingFontSize: 2.6525em; // 42px @@ -52,7 +53,7 @@ @searchBoxWidth: 375/16em; @iconSizeTotal: @iconSize + @iconGutterWidth + @iconGutterWidth; @deviceWidthTabletEms: unit( @width-breakpoint-tablet / @fontSizeBrowserDefault, em ); -@brandingBoxWidth: @deviceWidthTabletEms - ( @iconSizeTotal * 3 ) - @searchBoxWidth; +@brandingBoxWidth: @deviceWidthTabletEms - (3 * @menuButtonIconSize + @iconGutterWidth) - @searchBoxWidth; @titleSectionSpacingTop: 20px; @titleSectionSpacingBottom: 25px; diff --git a/resources/skins.minerva.amc.styles/userMenu.less b/resources/skins.minerva.amc.styles/userMenu.less new file mode 100644 index 0000000..1d9333b --- /dev/null +++ b/resources/skins.minerva.amc.styles/userMenu.less @@ -0,0 +1,11 @@ +@import '../../minerva.less/minerva.variables'; + +// The list that opens underneath the button. +.minerva-user-menu-list { + top: 100%; + right: 0; + margin-right: @contentMargin / 2; + min-width: 200px; + // A variable max-height is set in JavaScript so a minimum height is needed. + min-height: 200px; +} diff --git a/resources/skins.minerva.base.styles/userMenu.less b/resources/skins.minerva.base.styles/userMenu.less new file mode 100644 index 0000000..5980359 --- /dev/null +++ b/resources/skins.minerva.base.styles/userMenu.less @@ -0,0 +1,26 @@ +@import '../../minerva.less/minerva.variables'; + +// The top level user account button in the upper right near the notifications button. +.minerva-user-navigation { + display: flex; + height: @headerHeight; + // Fill empty space left / start so the buttons can stay right / end. + width: 100%; + // Keep space for at least two buttons. + min-width: 2 * @menuButtonIconSize; + // Center vertically. + align-items: center; + // Layout from right / end. + justify-content: flex-end; + // Show the list relative the button. + position: relative; + + .mw-ui-icon-element { + display: block; + } + + .toggle-list { + width: @menuButtonIconSize; + display: inline-block; + } +} diff --git a/resources/skins.minerva.mainMenu.icons/logOut-ltr.svg b/resources/skins.minerva.mainMenu.icons/logOut-ltr.svg new file mode 100644 index 0000000..3a7a533 --- /dev/null +++ b/resources/skins.minerva.mainMenu.icons/logOut-ltr.svg @@ -0,0 +1 @@ +log out \ No newline at end of file diff --git a/resources/skins.minerva.mainMenu.icons/logOut-rtl.svg b/resources/skins.minerva.mainMenu.icons/logOut-rtl.svg new file mode 100644 index 0000000..5261d66 --- /dev/null +++ b/resources/skins.minerva.mainMenu.icons/logOut-rtl.svg @@ -0,0 +1 @@ +log out \ No newline at end of file diff --git a/resources/skins.minerva.mainMenu.icons/userSandbox.svg b/resources/skins.minerva.mainMenu.icons/userSandbox.svg new file mode 100644 index 0000000..b3ceae9 --- /dev/null +++ b/resources/skins.minerva.mainMenu.icons/userSandbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/skins.minerva.scripts/init.js b/resources/skins.minerva.scripts/init.js index 98a18cc..876f23b 100644 --- a/resources/skins.minerva.scripts/init.js +++ b/resources/skins.minerva.scripts/init.js @@ -15,6 +15,7 @@ TitleUtil = require( './TitleUtil.js' ), issues = require( './page-issues/index.js' ), Toolbar = require( './Toolbar.js' ), + ToggleList = require( '../../components/ToggleList/ToggleList.js' ), router = require( 'mediawiki.router' ), CtaDrawer = mobile.CtaDrawer, Button = mobile.Button, @@ -341,7 +342,8 @@ $( function () { var $toc, - toolbarElement = document.querySelector( Toolbar.selector ); + toolbarElement = document.querySelector( Toolbar.selector ), + userMenu = document.querySelector( '.minerva-user-menu' ); // See UserMenuDirector. // Init: // - main menu closes when you click outside of it // - redirects show a toast. @@ -363,6 +365,10 @@ Toolbar.bind( window, toolbarElement, eventBus ); Toolbar.render( window, toolbarElement ); } + if ( userMenu ) { + ToggleList.bind( window, userMenu, eventBus, true ); + ToggleList.render( userMenu, true ); + } initRedlinksCta(); initUserRedLinks(); initTabsScrollPosition(); diff --git a/skin.json b/skin.json index d3cfbc3..e73f93b 100644 --- a/skin.json +++ b/skin.json @@ -203,6 +203,7 @@ "resources/skins.minerva.base.styles/reset.less", "resources/skins.minerva.base.styles/ui.less", "resources/skins.minerva.base.styles/pageactions.less", + "resources/skins.minerva.base.styles/userMenu.less", "resources/skins.minerva.base.styles/common.less", "resources/skins.minerva.base.styles/images.less", "resources/skins.minerva.base.styles/footer.less", @@ -259,7 +260,8 @@ "resources/skins.minerva.amc.styles/index.less", "components/ToggleList/ToggleList.less", "components/ToggleList/DropDownList.less", - "components/ToggleList/MenuListItem.less" + "components/ToggleList/MenuListItem.less", + "resources/skins.minerva.amc.styles/userMenu.less" ] }, "wikimedia.ui": { @@ -268,7 +270,7 @@ "defaultColor": "#54595d", "class": "ResourceLoaderOOUIIconPackModule", "icons": [ "articleRedirect", "info", "link", "listBullet", - "logoWikidata", "logoWikimedia", "quotes", "upload", "userAvatar" ] + "logoWikidata", "logoWikimedia", "quotes", "upload", "userAvatar", "userTalk" ] }, "skins.minerva.icons.images": { "class": "ResourceLoaderImageModule", @@ -387,7 +389,12 @@ "images": { "login": "resources/skins.minerva.mainMenu.icons/login.svg", "home": "resources/skins.minerva.mainMenu.icons/home.svg", - "logout": "resources/skins.minerva.mainMenu.icons/logout.svg", + "logout": { + "file": { + "ltr": "resources/skins.minerva.mainMenu.icons/logOut-ltr.svg", + "rtl": "resources/skins.minerva.mainMenu.icons/logOut-rtl.svg" + } + }, "nearby": "resources/skins.minerva.mainMenu.icons/nearby.svg", "random": "resources/skins.minerva.mainMenu.icons/random.svg", "settings": "resources/skins.minerva.mainMenu.icons/settings.svg", @@ -409,7 +416,8 @@ "ltr": "resources/skins.minerva.mainMenu.icons/communityportal-ltr.svg", "rtl": "resources/skins.minerva.mainMenu.icons/communityportal-rtl.svg" } - } + }, + "userSandbox": "resources/skins.minerva.mainMenu.icons/userSandbox.svg" } }, "skins.minerva.mainMenu.styles": {