| Current File : /home/jvzmxxx/wiki1/extensions/MobileFrontend/includes/specials/SpecialMobileWatchlist.php |
<?php
/**
* SpecialMobileWatchlist.php
*/
use MediaWiki\MediaWikiServices;
/**
* Implements the Watchlist special page
*/
class SpecialMobileWatchlist extends MobileSpecialPageFeed {
const LIMIT = 50; // Performance-safe value with PageImages
const THUMB_SIZE = MobilePage::SMALL_IMAGE_WIDTH;
const VIEW_OPTION_NAME = 'mfWatchlistView';
const FILTER_OPTION_NAME = 'mfWatchlistFilter';
/** @var string $view Saves, how the watchlist is sorted: a-z or as a feed */
private $view;
/**
* Construct function
*/
public function __construct() {
parent::__construct( 'Watchlist' );
}
/** @var string $filter Saves the actual used filter in feed view */
private $filter;
/** @var boolean $usePageImages Saves whether display images or not */
private $usePageImages;
/**
* @var boolean $optionsChanged Set true, when the user changed the
* view or feed of watchlist to save the new settings
*/
private $optionsChanged = false;
/** @var Title $fromPageTitle Saves the Title object of the page list starts from */
private $fromPageTitle;
/**
* Saves a user preference that reflects the current state of the watchlist
* e.g. whether it is the feed or A-Z view and which filters are currently applied.
*/
protected function updateStickyTabs() {
if ( $this->view === 'feed' ) {
// make filter stick on feed view
$this->updatePreference( self::FILTER_OPTION_NAME, $this->filter );
}
// make view sticky
$this->updatePreference( self::VIEW_OPTION_NAME, $this->view );
}
/**
* Render the special page
* @param string $par parameter submitted as subpage
*/
function executeWhenAvailable( $par ) {
// Anons don't get a watchlist
$this->requireLogin( 'mobile-frontend-watchlist-purpose' );
$ctx = MobileContext::singleton();
$this->usePageImages = !$ctx->imagesDisabled() && defined( 'PAGE_IMAGES_INSTALLED' );
$user = $this->getUser();
$output = $this->getOutput();
$output->addModules( 'skins.minerva.special.watchlist.scripts' );
// FIXME: Loads twice with JS enabled (T87871)
$output->addModuleStyles( [
'skins.minerva.special.watchlist.styles',
'mobile.pagelist.styles',
'mobile.pagesummary.styles',
] );
$req = $this->getRequest();
$this->view = $req->getVal( 'watchlistview', 'a-z' );
$this->filter = $req->getVal( 'filter', 'all' );
$this->fromPageTitle = Title::newFromText( $req->getVal( 'from', false ) );
$output->setPageTitle( $this->msg( 'watchlist' ) );
// This needs to be done before calling getWatchlistHeader
$this->updateStickyTabs();
if ( $this->optionsChanged ) {
DeferredUpdates::addCallableUpdate( function() use ( $user ) {
$user->saveSettings();
} );
}
if ( $this->view === 'feed' ) {
$output->addHtml( $this->getWatchlistHeader( $user ) );
$output->addHtml(
Html::openElement( 'div', [ 'class' => 'content-unstyled' ] )
);
$this->showRecentChangesHeader();
$res = $this->doFeedQuery();
if ( $res->numRows() ) {
$this->showFeedResults( $res );
} else {
$this->showEmptyList( true );
}
$output->addHtml(
Html::closeElement( 'div' )
);
} else {
$output->redirect( SpecialPage::getTitleFor( 'EditWatchlist' )->getLocalURL() );
}
}
/**
* Returns an array of conditions restricting namespace in queries
* @param string $column Namespace db key
*
* @return array
*/
protected function getNSConditions( $column ) {
$conds = [];
switch ( $this->filter ) {
case 'all':
// no-op
break;
case 'articles':
// @fixme content namespaces
$conds[] = "$column = 0"; // Has to be unquoted or MySQL will filesort for wl_namespace
break;
case 'talk':
// check project talk, user talk and talk pages
$conds[] = "$column IN (1, 3, 5)";
break;
case 'other':
// @fixme
$conds[] = "$column NOT IN (0, 1, 3, 5)";
break;
}
return $conds;
}
/**
* Get the header for the watchlist page
* @param User $user
* @param string|null $view the name of the view to show (optional)
* If absent user preferences will be consulted.
* @return string Parsed HTML
*/
public static function getWatchlistHeader( User $user, $view = null ) {
$sp = SpecialPage::getTitleFor( 'Watchlist' );
$attrsList = $attrsFeed = [];
// https://phabricator.wikimedia.org/T150650
if ( $view === null ) {
$view = $user->getOption( SpecialMobileWatchlist::VIEW_OPTION_NAME, 'a-z' );
}
$filter = $user->getOption( SpecialMobileWatchlist::FILTER_OPTION_NAME, 'all' );
if ( $view === 'feed' ) {
$attrsList[ 'class' ] = MobileUI::buttonClass();
// FIXME [MediaWiki UI] This probably be described as a different type of mediawiki ui element
$attrsFeed[ 'class' ] = MobileUI::buttonClass( 'progressive', 'is-on' );
} else {
$attrsFeed[ 'class' ] = MobileUI::buttonClass();
// FIXME [MediaWiki UI] This probably be described as a different type of mediawiki ui element
$attrsList[ 'class' ] = MobileUI::buttonClass( 'progressive', 'is-on' );
}
$linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
$html =
Html::openElement( 'ul', [ 'class' => 'button-bar mw-ui-button-group' ] ) .
Html::openElement( 'li', $attrsList ) .
$linkRenderer->makeLink( $sp,
wfMessage( 'mobile-frontend-watchlist-a-z' )->text(),
[ 'class' => 'button' ],
[ 'watchlistview' => 'a-z' ]
) .
Html::closeElement( 'li' ) .
Html::openElement( 'li', $attrsFeed ) .
$linkRenderer->makeLink( $sp,
wfMessage( 'mobile-frontend-watchlist-feed' )->text(),
[ 'class' => 'button' ],
[ 'watchlistview' => 'feed', 'filter' => $filter ]
) .
Html::closeElement( 'li' ) .
Html::closeElement( 'ul' );
return '<div class="content-header">' . $html . '</div>';
}
/**
* Render "second" header for filter in feed view of watchlist
*/
function showRecentChangesHeader() {
$filters = [
'all' => 'mobile-frontend-watchlist-filter-all',
'articles' => 'mobile-frontend-watchlist-filter-articles',
'talk' => 'mobile-frontend-watchlist-filter-talk',
'other' => 'mobile-frontend-watchlist-filter-other',
];
$output = $this->getOutput();
$output->addHtml(
Html::openElement( 'ul', [ 'class' => 'mw-mf-watchlist-selector page-header-bar' ] )
);
foreach ( $filters as $filter => $msg ) {
$itemAttrs = [];
if ( $filter === $this->filter ) {
$itemAttrs['class'] = 'selected';
}
$linkAttrs = [
'href' => $this->getPageTitle()->getLocalUrl(
[
'filter' => $filter,
'watchlistview' => 'feed',
]
)
];
$output->addHtml(
Html::openElement( 'li', $itemAttrs ) .
Html::element( 'a', $linkAttrs, $this->msg( $msg )->plain() ) .
Html::closeElement( 'li' )
);
}
$output->addHtml(
Html::closeElement( 'ul' )
);
}
/**
* Get watchlist items for feed view
* @return ResultWrapper
*
* @see getNSConditions()
* @see doPageImages()
*/
protected function doFeedQuery() {
$user = $this->getUser();
$dbr = wfGetDB( DB_SLAVE, 'watchlist' );
// Possible where conditions
$conds = $this->getNSConditions( 'rc_namespace' );
// snip....
$tables = [ 'recentchanges', 'watchlist' ];
$fields = [ $dbr->tableName( 'recentchanges' ) . '.*' ];
$innerConds = [
'wl_user' => $user->getId(),
'wl_namespace=rc_namespace',
'wl_title=rc_title',
// FIXME: Filter out wikidata changes which currently show as anonymous (see bug 49315)
'rc_type!=' . $dbr->addQuotes( RC_EXTERNAL ),
];
// Filter out category membership changes if configured
if ( $user->getBoolOption( 'hidecategorization' ) ) {
$innerConds[] = 'rc_type!=' . $dbr->addQuotes( RC_CATEGORIZE );
}
$join_conds = [
'watchlist' => [
'INNER JOIN',
$innerConds,
],
];
$options = [ 'ORDER BY' => 'rc_timestamp DESC' ];
$options['LIMIT'] = self::LIMIT;
$rollbacker = $user->isAllowed( 'rollback' );
if ( $rollbacker ) {
$tables[] = 'page';
$join_conds['page'] = [ 'LEFT JOIN', 'rc_cur_id=page_id' ];
if ( $rollbacker ) {
$fields[] = 'page_latest';
}
}
ChangeTags::modifyDisplayQuery( $tables, $fields, $conds, $join_conds, $options, '' );
// Until 1.22, MediaWiki used an array here. Since 1.23 (Iec4aab87), it uses a FormOptions
// object (which implements array-like interface ArrayAccess).
// Let's keep using an array and hope any new extensions are compatible with both styles...
$values = [];
Hooks::run(
'SpecialWatchlistQuery',
[ &$conds, &$tables, &$join_conds, &$fields, &$values ]
);
$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $join_conds );
return $res;
}
/**
* Show results of doFeedQuery
* @param ResultWrapper $res ResultWrapper returned from db
*
* @see showResults()
*/
protected function showFeedResults( ResultWrapper $res ) {
$this->showResults( $res, true );
}
/**
* Render the Watchlist items.
* When ?from not set, adds a link "more" to see the other watchlist items.
* @param ResultWrapper $res ResultWrapper from db
* @param boolean $feed Render as feed (true) or list (false) view?
* @todo FIXME: use templates/PageList.html when server side templates
* are available to keep consistent with nearby view
*/
protected function showResults( ResultWrapper $res, $feed ) {
$output = $this->getOutput();
if ( $feed ) {
foreach ( $res as $row ) {
$this->showFeedResultRow( $row );
}
}
$output->addHtml( '</ul>' );
}
/**
* If the user doesn't watch any page, show information how to watch some.
* @param boolean $feed Render as feed (true) or list (false) view?
*/
function showEmptyList( $feed ) {
$this->getOutput()->addHtml( self::getEmptyListHtml( $feed, $this->getLanguage() ) );
}
/**
* Get the HTML needed to show if a user doesn't watch any page, show information
* how to watch pages where no pages have been watched.
* @param boolean $feed Render as feed (true) or list (false) view?
* @param Language $lang The language of the current mode
* @return string
*/
public static function getEmptyListHtml( $feed, $lang ) {
$dir = $lang->isRTL() ? 'rtl' : 'ltr';
$imgUrl = MobileContext::singleton()->getConfig()->get( 'ExtensionAssetsPath' ) .
"/MobileFrontend/images/emptywatchlist-page-actions-$dir.png";
if ( $feed ) {
$msg = Html::element( 'p', null, wfMessage( 'mobile-frontend-watchlist-feed-empty' )->plain() );
} else {
$msg = Html::element( 'p', null,
wfMessage( 'mobile-frontend-watchlist-a-z-empty-howto' )->plain()
);
$msg .= Html::element( 'img', [
'src' => $imgUrl,
'alt' => wfMessage( 'mobile-frontend-watchlist-a-z-empty-howto-alt' )->plain(),
] );
}
return Html::openElement( 'div', [ 'class' => 'info empty-page' ] ) .
$msg .
Html::element( 'a',
[ 'class' => 'button', 'href' => Title::newMainPage()->getLocalUrl() ],
wfMessage( 'mobile-frontend-watchlist-back-home' )->plain()
) .
Html::closeElement( 'div' );
}
/**
* Render a result row in feed view
* @param object $row a row of db result
*/
protected function showFeedResultRow( $row ) {
if ( $row->rc_deleted ) {
return;
}
$user = $this->getUser();
$lang = $this->getLanguage();
$date = $lang->userDate( $row->rc_timestamp, $user );
$this->renderListHeaderWhereNeeded( $date );
$title = Title::makeTitle( $row->rc_namespace, $row->rc_title );
$comment = $this->formatComment( $row->rc_comment, $title );
$ts = new MWTimestamp( $row->rc_timestamp );
$username = $row->rc_user != 0
? htmlspecialchars( $row->rc_user_text )
: IP::prettifyIP( $row->rc_user_text );
$revId = $row->rc_this_oldid;
$bytes = $row->rc_new_len - $row->rc_old_len;
$isAnon = $row->rc_user == 0;
$isMinor = $row->rc_minor != 0;
if ( $revId ) {
$diffTitle = SpecialPage::getTitleFor( 'MobileDiff', $revId );
$diffLink = $diffTitle->getLocalUrl();
} else {
// hack -- use full log entry display
$diffLink = Title::makeTitle( $row->rc_namespace, $row->rc_title )->getLocalUrl();
}
$this->renderFeedItemHtml( $ts, $diffLink, $username, $comment, $title, $isAnon, $bytes,
$isMinor );
}
/**
* Save the settings for the watchlist, so if the user comes back
* later he see the same filter and list view
* @param string $name The name of the option to set
* @param string|boolean $value The value for the option to set
*/
private function updatePreference( $name, $value ) {
$user = $this->getUser();
if ( $user->getOption( $name ) != $value ) {
$user->setOption( $name, $value );
$this->optionsChanged = true;
}
}
/**
* Formats a comment of revision via Linker:formatComment and Sanitizer::stripAllTags
* @param string $comment the comment
* @param string $title the title object of comments page
* @return string formatted comment
*/
protected function formatComment( $comment, $title ) {
if ( $comment !== '' ) {
$comment = Linker::formatComment( $comment, $title );
// flatten back to text
$comment = Sanitizer::stripAllTags( $comment );
}
return $comment;
}
}