jenkins-bot has submitted this change and it was merged. Change subject: Story 475: UserProfile special page (first pass) ......................................................................
Story 475: UserProfile special page (first pass) Change-Id: I59ed57fcd2c75788f42d6d306cba81719c964204 --- M MobileFrontend.alias.php M MobileFrontend.i18n.php M MobileFrontend.php M includes/MobileFrontend.hooks.php M includes/Resources.php M includes/skins/MinervaTemplate.php M includes/skins/SkinMinerva.php M includes/skins/SkinMobile.php A includes/specials/SpecialUserProfile.php M less/common/pageactions.less M less/common/ui.less A less/specials/userprofile.less M stylesheets/common/pageactions.css M stylesheets/common/ui.css A stylesheets/specials/userprofile.css 15 files changed, 368 insertions(+), 10 deletions(-) Approvals: Siebrand: Looks good to me, but someone else must approve MaxSem: Looks good to me, approved jenkins-bot: Verified diff --git a/MobileFrontend.alias.php b/MobileFrontend.alias.php index 7c990e8..37e3d5f 100644 --- a/MobileFrontend.alias.php +++ b/MobileFrontend.alias.php @@ -15,6 +15,7 @@ 'MobileDiff' => array( 'MobileDiff' ), 'MobileMenu' => array( 'MobileMenu' ), 'Nearby' => array( 'Nearby' ), + 'UserProfile' => array( 'UserProfile' ), ); /** Arabic (العربية) */ diff --git a/MobileFrontend.i18n.php b/MobileFrontend.i18n.php index 1b90a50..d7a09dd 100644 --- a/MobileFrontend.i18n.php +++ b/MobileFrontend.i18n.php @@ -301,6 +301,23 @@ // AbuseFilter variable 'abusefilter-edit-builder-vars-user-mobile' => 'Whether or not a user is editing through the mobile interface', + + // Special:UserProfile + 'mobile-frontend-profile-title' => 'User profile', + 'mobile-frontend-profile-edits' => '{{PLURAL:$1|1 edit|$1 edits|0=No edits}} in last month', + 'mobile-frontend-profile-uploads' => '{{PLURAL:$1|1 upload|$1 uploads|0=No uploads}} in last month', + 'mobile-frontend-profile-edits-limit' => 'Over {{PLURAL:$1|$1 edit|$1 edits}} in last month', + 'mobile-frontend-profile-uploads-limit' => 'Over {{PLURAL:$1|$1 upload|$1 uploads}} in last month', + 'mobile-frontend-profile-upload-caption' => '$1 was the last upload by {{GENDER:$2|$2}} and was uploaded {{PLURAL:$3|$3 day ago|$3 days ago}}.', + 'mobile-frontend-profile-heading-recent' => 'Recent', + 'mobile-frontend-profile-registered' => 'Member for {{PLURAL:$1|$1 day|$1 days}} with {{PLURAL:$2|$2 edit|$2 edits}}.', + 'mobile-frontend-profile-usertalk' => 'View talk page.', + 'mobile-frontend-profile-noargs' => 'Please provide a username to view a profile.', + 'mobile-frontend-profile-yours' => 'Visit your profile page.', + 'mobile-frontend-profile-user-desc-1' => '{{GENDER:$1|This user}} is a new editor.', + 'mobile-frontend-profile-user-desc-2' => '{{GENDER:$1|This user}} is a somewhat experienced editor.', + 'mobile-frontend-profile-user-desc-3' => '{{GENDER:$1|This user}} is a profilic editor.', + 'mobile-frontend-requires-optin' => 'This page is not available unless you opt into our beta mode. Visit the [[Special:MobileOptions|settings page]] to opt in.' ); /** Message documentation (Message documentation) @@ -762,6 +779,22 @@ 'mobile-frontend-media-details' => 'Caption for a button leading to the details of a media file (e.g. an image) in a preview. {{Identical|Detail}}', 'abusefilter-edit-builder-vars-user-mobile' => 'See {{msg-mw|Abusefilter-edit-builder-vars-user-name}} (from AbuseFilter extension), for example.', + + 'mobile-frontend-profile-title' => 'Title of the Special:UserProfile page', + 'mobile-frontend-profile-edits' => 'Edit count. $1 is number of edits in the last month.', + 'mobile-frontend-profile-uploads' => 'Upload count in the last month.', + 'mobile-frontend-profile-edits-limit' => 'Edit count when expressed to be over a certain number.', + 'mobile-frontend-profile-uploads-limit' => 'Upload count when expressed to be over a certain number.', + 'mobile-frontend-profile-upload-caption' => '$1 is the filename, $2 is the username and $3 is the amount of days ago the image was uploaded.', + 'mobile-frontend-profile-heading-recent' => 'Heading for recent section profile.', + 'mobile-frontend-profile-registered' => 'Summary saying how many days the user has been a member ($1) and how many total edits they have made ($2).', + 'mobile-frontend-profile-usertalk' => 'Link label to user talk page.', + 'mobile-frontend-profile-noargs' => 'Message shown when no user profile URL is specified.', + 'mobile-frontend-profile-yours' => 'Label pointing user to their profile.', + 'mobile-frontend-profile-user-desc-1' => 'Summary describing that this user is new.', + 'mobile-frontend-profile-user-desc-2' => 'Summary describing that the user is somewhat experienced.', + 'mobile-frontend-profile-user-desc-3' => 'Summary describing the type of experience the user has.', + 'mobile-frontend-requires-optin' => 'Message that shows when a page requires beta mode to work. Wikitext that links to [[Special:MobileOptions]] page.', ); /** Achinese (Acèh) diff --git a/MobileFrontend.php b/MobileFrontend.php index 187f262..14bc138 100644 --- a/MobileFrontend.php +++ b/MobileFrontend.php @@ -55,6 +55,7 @@ 'MobileSiteModule' => 'modules/MobileSiteModule', 'SpecialUploads' => 'specials/SpecialUploads', + 'SpecialUserProfile' => 'specials/SpecialUserProfile', 'SpecialMobileUserlogin' => 'specials/SpecialMobileUserlogin', 'SpecialMobileDiff' => 'specials/SpecialMobileDiff', 'SpecialMobileOptions' => 'specials/SpecialMobileOptions', diff --git a/includes/MobileFrontend.hooks.php b/includes/MobileFrontend.hooks.php index d5a4caa..6ea9b93 100644 --- a/includes/MobileFrontend.hooks.php +++ b/includes/MobileFrontend.hooks.php @@ -268,7 +268,8 @@ * @return boolean hook return value */ public static function onSpecialPage_initList( &$list ) { - if ( MobileContext::singleton()->shouldDisplayMobileView() ) { + $ctx = MobileContext::singleton(); + if ( $ctx->shouldDisplayMobileView() ) { // Replace the standard watchlist view with our custom one $list['Watchlist'] = 'SpecialMobileWatchlist'; // FIXME: Make uploads work on desktop @@ -278,6 +279,8 @@ if ( class_exists( 'MWEchoNotifUser' ) ) { $list['Notifications'] = 'SpecialMobileNotifications'; } + + $list['UserProfile'] = 'SpecialUserProfile'; } return true; } diff --git a/includes/Resources.php b/includes/Resources.php index 8d6a348..bbda05a 100644 --- a/includes/Resources.php +++ b/includes/Resources.php @@ -721,7 +721,12 @@ 'stylesheets/specials/userlogin.css', ), ), - + // Special:UserProfile + 'mobile.userprofile.styles' => $wgMFMobileSpecialPageResourceBoilerplate + array( + 'styles' => array( + 'stylesheets/specials/userprofile.css', + ), + ), // Special:Uploads 'mobile.special.uploads.plumbing' => $wgMFMobileResourceTemplateBoilerplate + array( 'templates' => array( diff --git a/includes/skins/MinervaTemplate.php b/includes/skins/MinervaTemplate.php index ce3b9dd..c6341b2 100644 --- a/includes/skins/MinervaTemplate.php +++ b/includes/skins/MinervaTemplate.php @@ -190,7 +190,7 @@ </form> <?php } - echo $data['userButton']; + echo $data['secondaryButton']; ?> </div> <script> diff --git a/includes/skins/SkinMinerva.php b/includes/skins/SkinMinerva.php index 01c6c71..bee8a6f 100644 --- a/includes/skins/SkinMinerva.php +++ b/includes/skins/SkinMinerva.php @@ -30,17 +30,18 @@ // FIXME: cap higher counts $count = $user->isLoggedIn() ? MWEchoNotifUser::newFromUser( $user )->getNotificationCount() : 0; - $tpl->set( 'userButton', + $tpl->set( 'secondaryButton', Html::openElement( 'a', array( 'title' => wfMessage( 'mobile-frontend-user-button-tooltip' ), 'href' => SpecialPage::getTitleFor( 'Notifications' )->getLocalURL( array( 'returnto' => $this->getTitle()->getPrefixedText() ) ), - 'id'=> 'user-button', + 'class' => 'user-button', + 'id'=> 'secondary-button', ) ) . Html::element( 'span', array( 'class' => $count ? '' : 'zero' ), $count ) . Html::closeElement( 'a' ) ); } else { - $tpl->set( 'userButton', '' ); + $tpl->set( 'secondaryButton', '' ); } } diff --git a/includes/skins/SkinMobile.php b/includes/skins/SkinMobile.php index 56a5568..a6db543 100644 --- a/includes/skins/SkinMobile.php +++ b/includes/skins/SkinMobile.php @@ -6,6 +6,7 @@ protected $hookOptions; protected $mode = 'stable'; + protected $customisations = array(); /** @var array of classes that should be present on the body tag */ private $pageClassNames = array(); @@ -34,6 +35,16 @@ 'data-section' => $section, 'class' => 'edit-page' ), $message ); + } + + public function setTemplateVariable( $key, $val ) { + $this->customisations[$key] = $val; + } + + private function applyCustomisations( $tpl ) { + foreach( $this->customisations as $key => $value ) { + $tpl->set( $key, $value ); + } } public function outputPage( OutputPage $out = null ) { @@ -109,6 +120,7 @@ $search = $tpl->data['searchBox']; $search['placeholder'] = $this->getSearchPlaceHolderText(); $tpl->set( 'searchBox', $search ); + $this->applyCustomisations( $tpl ); } public function getSkinConfigVariables() { diff --git a/includes/specials/SpecialUserProfile.php b/includes/specials/SpecialUserProfile.php new file mode 100644 index 0000000..78b2485 --- /dev/null +++ b/includes/specials/SpecialUserProfile.php @@ -0,0 +1,223 @@ +<?php + +class SpecialUserProfile extends MobileSpecialPage { + const LIMIT = 500; + + public function __construct() { + parent::__construct( 'UserProfile' ); + } + + protected function getDaysAgo( $ts ) { + $now = new MWTimestamp(); + $diff = $ts->diff( $now ); + return $diff->days; + } + + protected function getListHtml( $tag, $attrs, $items ) { + $html = Html::openElement( $tag, $attrs ); + foreach( $items as $item ) { + $html .= Html::openElement( 'li' ); + $html .= $item; + $html .= Html::closeElement( 'li' ); + } + $html .= Html::closeElement( $tag ); + return $html; + } + + /** + * Returns a count of the most recent edits since a given timestamp + * + * @param User $user The user name to query against + * @param Integer $fromDate Time to measure from + * @return Integer the amount of edits + */ + protected function countRecentEdits( $user, $fromDate ) { + $dbr = wfGetDB( DB_SLAVE ); + $where = array( + 'rc_user_text' => $user->getName(), + ); + $where[] = 'rc_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $fromDate ) ); + $constraints = array( + 'limit' => self::LIMIT + 1, + ); + $res = $dbr->select( 'recentchanges', 'rc_timestamp', $where, __METHOD__, $constraints ); + return $res->numRows(); + } + + /** + * Returns a count of the most recent uploads to $wgMFPhotoUploadWiki since a given timestamp + * + * @param User $user The user name to query against + * @param Integer $fromDate Time to measure from + * @return Integer the amount of edits + */ + protected function countRecentUploads( $user, $fromDate ) { + global $wgMFPhotoUploadWiki, $wgConf; + + if ( !$wgMFPhotoUploadWiki ) { + $dbr = wfGetDB( DB_SLAVE ); + } elseif ( + $wgMFPhotoUploadWiki && + !in_array( $wgMFPhotoUploadWiki, $wgConf->getLocalDatabases() ) + ) { + // early return if the database is invalid + return false; + } else { + $dbr = wfGetDB( DB_SLAVE, array(), $wgMFPhotoUploadWiki ); + } + + $where = array( 'img_user_text' => $user->getName() ); + $where[] = 'img_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $fromDate ) ); + $constraints = array( + 'limit' => self::LIMIT + 1, + ); + $res = $dbr->select( 'image', 'img_timestamp', $where, __METHOD__, $constraints ); + return $res->numRows(); + } + + /** + * Returns HTML to show the last upload or a message where there is no last upload + * + * @param User $user The user name to query against + * @return String HTML string representing the last upload by the user + */ + protected function getLastUpload( $user ) { + global $wgMFPhotoUploadWiki, $wgConf; + + if ( !$wgMFPhotoUploadWiki ) { + $dbr = wfGetDB( DB_SLAVE ); + } elseif ( + $wgMFPhotoUploadWiki && + !in_array( $wgMFPhotoUploadWiki, $wgConf->getLocalDatabases() ) + ) { + // early return if the database is invalid + return false; + } else { + $dbr = wfGetDB( DB_SLAVE, array(), $wgMFPhotoUploadWiki ); + } + + $where = array( 'img_user_text' => $user->getName() ); + $constraints = array( 'LIMIT' => 1, 'ORDER BY' => 'img_timestamp DESC' ); + $res = $dbr->select( 'image', 'img_name, img_timestamp', $where, __METHOD__, $constraints ); + foreach( $res as $row ) { + $name = $row->img_name; + $file = wfLocalFile( $name ); + $title = Title::newFromText( $name, NS_FILE ); + $ts = new MWTimestamp( wfTimestamp( TS_UNIX, $row->img_timestamp ) ); + $daysAgo = $this->getDaysAgo( $ts ); + + $img = Html::openElement( 'div', array( 'class' => 'thumb' ) ) . + Html::openElement( 'a', array( 'href' => $title->getLocalUrl() ) ) . + $file->transform( array( 'width' => 320, 'height' => 320 ) )->toHtml() . + Html::openElement( 'div', array( 'class' => 'thumbcaption' ) ) . + $this->msg( 'mobile-frontend-profile-upload-caption', $name, $user, $daysAgo )->parse() . + Html::closeElement( 'div' ) . + Html::closeElement( 'a' ) . + Html::closeElement( 'div' ); + return $img; + } + return ''; + } + + protected function getUserSummary( User $user ) { + $registered = $user->getRegistration(); + $editCount = $user->getEditCount(); + $ts = new MWTimestamp( wfTimestamp( TS_UNIX, $registered ) ); + $daysAgo = $this->getDaysAgo( $ts ); + + if( $editCount < 50 ) { + $role = $this->msg( 'mobile-frontend-profile-user-desc-1', $user ); + } else if ( $editCount < 5000 ) { + $role = $this->msg( 'mobile-frontend-profile-user-desc-2', $user ); + } else { + $role = $this->msg( 'mobile-frontend-profile-user-desc-3', $user ); + } + + return Html::element( 'p', array( 'class' => 'statement' ), + $this->msg( 'mobile-frontend-profile-registered', $daysAgo, $editCount )->parse() ) . + Html::element( 'p', array( 'class' => 'secondary-statement' ), $role ); + } + + protected function getRecentActivityHtml( User $user ) { + // render + $fromDate = time() - ( 3600 * 24 * 30 ); + $count = $this->countRecentEdits( $user, $fromDate ); + $uploadCount = $this->countRecentUploads( $user, $fromDate ); + + $urlContributions = SpecialPage::getTitleFor( 'Contributions', $user->getName() )->getLocalUrl(); + $urlUploads = SpecialPage::getTitleFor( 'Uploads', $user->getName() )->getLocalUrl(); + $msgUploads = $uploadCount > self::LIMIT ? $this->msg( 'mobile-frontend-profile-uploads-limit', self::LIMIT ) : + $this->msg( 'mobile-frontend-profile-uploads', $uploadCount ); + $msgEdits = $count > self::LIMIT ? $this->msg( 'mobile-frontend-profile-edits-limit', self::LIMIT ) : + $this->msg( 'mobile-frontend-profile-edits', $count ); + $statsRecent = array( + Html::element( 'a', array( 'href' => $urlContributions ), $msgEdits ), + Html::element( 'a', array( 'href' => $urlUploads ), $msgUploads ), + ); + $lastUploadHtml = $this->getLastUpload( $user ); + if ( $lastUploadHtml ) { + $statsRecent[] = $lastUploadHtml; + } + + $html = Html::element( 'h2', array(), $this->msg( 'mobile-frontend-profile-heading-recent' ) ) . + $this->getListHtml( 'ul', array( 'class' => 'statements' ), $statsRecent ); + + return $html; + } + + protected function setUserProfileUIElements( User $user ) { + // replace secondary icon + $attrs = array( + 'class' => 'talk', + 'id' => 'secondary-button', + 'href' => $user->getTalkPage()->getLocalUrl(), + ); + $secondaryButton = Html::element( 'a', $attrs, $this->msg( 'mobile-frontend-profile-usertalk' ) ); + + // define heading + $userPageLink = Html::element( 'a', array( 'href' => $user->getUserPage()->getLocalUrl() ), $user->getName() ); + $heading = Html::openElement( 'h1', array() ) . $userPageLink . Html::closeElement( 'h1' ); + + // set values + $skin = $this->getSkin(); + $skin->setTemplateVariable( 'secondaryButton', $secondaryButton ); + $skin->setTemplateVariable( 'specialPageHeader', $heading ); + } + + public function getHtmlNoArg() { + $html = Html::element( 'p', array(), $this->msg( 'mobile-frontend-profile-noargs' ) ); + $user = $this->getUser(); + if ( $user->isLoggedIn() ) { + $profileUrl = SpecialPage::getTitleFor( $this->getName(), $user->getName() )->getLocalURL(); + $html .= Html::openElement( 'p', array() ); + $html .= Html::element( 'a', array( 'href' => $profileUrl ), $this->msg( 'mobile-frontend-profile-yours' ) ); + $html .= Html::closeElement( 'p', array() ); + } + return $html; + } + + public function getHtmlBetaAlphaOptIn() { + return Html::openElement( 'div', array( 'class' => 'alert warning' ) ) . + wfMessage( 'mobile-frontend-requires-optin' )->parse() . + Html::closeElement( 'div' ); + } + + public function execute( $par = '' ) { + $out = $this->getOutput(); + $this->addModules(); + $out->setPageTitle( $this->msg( 'mobile-frontend-profile-title' ) ); + $ctx = MobileContext::singleton(); + if ( !$ctx->isBetaGroupMember() ) { + $html = $this->getHtmlBetaAlphaOptIn(); + } else if ( $par ) { + $user = User::newFromName( $par ); + // prepare content + $this->setUserProfileUIElements( $user ); + $html = Html::openElement( 'div', array( 'class' => 'profile' ) ) . + $this->getUserSummary( $user ) . $this->getRecentActivityHtml( $user ) . '</div>'; + } else { + $html = $this->getHtmlNoArg(); + } + $out->addHtml( $html ); + } +} diff --git a/less/common/pageactions.less b/less/common/pageactions.less index 159aa36..bc68fc1 100644 --- a/less/common/pageactions.less +++ b/less/common/pageactions.less @@ -77,9 +77,19 @@ #ca-talk { margin-right: 14px; + // FIXME: remove in favour of using .talk class background-image: url(images/pagemenu/talk.png); } +.talk { + background-image: url(images/pagemenu/talk.png); + // FIXME: use generic rule for following 3 rules; + background-repeat: no-repeat; + background-position: center center; + text-indent: -999px; + overflow: hidden; +} + #ca-upload { margin-right: 19px; background-image: url(images/pagemenu/upload-locked.png); diff --git a/less/common/ui.less b/less/common/ui.less index ad4403d..dcbff4b 100644 --- a/less/common/ui.less +++ b/less/common/ui.less @@ -26,12 +26,21 @@ /* Menu buttons */ #mw-mf-menu-page, #mw-mf-main-menu-button, +#secondary-button, +// FIXME: rename this to secondary-button #user-button { display: inline-block; //? height: @headerHeight; width: @searchBarPaddingLeft; position: absolute; top: 0; +} + +// FIXME: use the class after cache has cleared +#user-button, +.user-button { + display: none; + background: url(images/user.png) 50% 50% no-repeat; } /* Echo Notifications */ @@ -44,10 +53,10 @@ } } +#secondary-button, +// FIXME: rename this to secondary-button #user-button { - display: none; right: 0; - background: url(images/user.png) 50% 50% no-repeat; .background-size( auto, 24px ); span { diff --git a/less/specials/userprofile.less b/less/specials/userprofile.less new file mode 100644 index 0000000..c509b66 --- /dev/null +++ b/less/specials/userprofile.less @@ -0,0 +1,25 @@ +.profile { + .statements li, + .statement { + font-family: Georgia; + font-size: 1.95em; + margin-bottom: 0; + } + .secondary-statement { + font-family: Arial; + font-style: italic; + font-size: 21px; + color: #919191; + } +} + +.content h2 { + font-family: Arial; + font-size: 1.47em; + text-transform: uppercase; + color: rgb(145, 145, 145); +} + +ul { + list-style: none; +} diff --git a/stylesheets/common/pageactions.css b/stylesheets/common/pageactions.css index fc0594e..f8df77b 100644 --- a/stylesheets/common/pageactions.css +++ b/stylesheets/common/pageactions.css @@ -72,6 +72,13 @@ margin-right: 14px; background-image: url(images/pagemenu/talk.png); } +.talk { + background-image: url(images/pagemenu/talk.png); + background-repeat: no-repeat; + background-position: center center; + text-indent: -999px; + overflow: hidden; +} #ca-upload { margin-right: 19px; background-image: url(images/pagemenu/upload-locked.png); diff --git a/stylesheets/common/ui.css b/stylesheets/common/ui.css index 6a511f5..7f3a88f 100644 --- a/stylesheets/common/ui.css +++ b/stylesheets/common/ui.css @@ -338,12 +338,18 @@ /* Menu buttons */ #mw-mf-menu-page, #mw-mf-main-menu-button, +#secondary-button, #user-button { display: inline-block; height: 46px; width: 40px; position: absolute; top: 0; +} +#user-button, +.user-button { + display: none; + background: url(images/user.png) 50% 50% no-repeat; } /* Echo Notifications */ .is-authenticated .header { @@ -352,16 +358,16 @@ .is-authenticated #user-button { display: block; } +#secondary-button, #user-button { - display: none; right: 0; - background: url(images/user.png) 50% 50% no-repeat; /* use -webkit prefix for older android browsers eg. nexus 1 */ -o-background-size: auto 24px; -webkit-background-size: auto 24px; background-size: auto 24px; } +#secondary-button span, #user-button span { position: absolute; right: 5px; @@ -374,6 +380,7 @@ background: #c91f2c; border-radius: 3px; } +#secondary-button span.zero, #user-button span.zero { display: none; } diff --git a/stylesheets/specials/userprofile.css b/stylesheets/specials/userprofile.css new file mode 100644 index 0000000..7dbbae9 --- /dev/null +++ b/stylesheets/specials/userprofile.css @@ -0,0 +1,21 @@ +.profile .statements li, +.profile .statement { + font-family: Georgia; + font-size: 1.95em; + margin-bottom: 0; +} +.profile .secondary-statement { + font-family: Arial; + font-style: italic; + font-size: 21px; + color: #919191; +} +.content h2 { + font-family: Arial; + font-size: 1.47em; + text-transform: uppercase; + color: #919191; +} +ul { + list-style: none; +} -- To view, visit https://gerrit.wikimedia.org/r/76271 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I59ed57fcd2c75788f42d6d306cba81719c964204 Gerrit-PatchSet: 10 Gerrit-Project: mediawiki/extensions/MobileFrontend Gerrit-Branch: master Gerrit-Owner: Jdlrobson <jrob...@wikimedia.org> Gerrit-Reviewer: Asher <afeld...@wikimedia.org> Gerrit-Reviewer: JGonera <jgon...@wikimedia.org> Gerrit-Reviewer: Kaldari <rkald...@wikimedia.org> Gerrit-Reviewer: Maryana <mpinc...@wikimedia.org> Gerrit-Reviewer: MaxSem <maxsem.w...@gmail.com> Gerrit-Reviewer: Siebrand <siebr...@wikimedia.org> Gerrit-Reviewer: Springle <sprin...@wikimedia.org> Gerrit-Reviewer: jenkins-bot _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits