45 public function __construct( $page =
'Watchlist', $restriction =
'viewmywatchlist' ) {
46 parent::__construct( $page, $restriction );
48 $this->maxDays = $this->
getConfig()->get(
'RCMaxAge' ) / ( 3600 * 24 );
49 $this->watchStore = MediaWikiServices::getInstance()->getWatchedItemStore();
68 $output->addModuleStyles( [
'mediawiki.special' ] );
70 'mediawiki.special.recentchanges',
71 'mediawiki.special.watchlist',
75 if ( $mode !==
false ) {
95 if ( ( $config->get(
'EnotifWatchlist' ) || $config->get(
'ShowUpdatedMarker' ) )
96 && $request->getVal(
'reset' )
97 && $request->wasPosted()
98 && $user->matchEditToken( $request->getVal(
'token' ) )
100 $user->clearAllNotifications();
106 parent::execute( $subpage );
109 $output->addModuleStyles( [
'mediawiki.rcfilters.highlightCircles.seenunseen.styles' ] );
117 if ( $user instanceof
Config ) {
118 wfDeprecated( __METHOD__ .
' with Config argument',
'1.34' );
119 $user = func_get_arg( 1 );
121 return !$user->getOption(
'wlenhancedfilters-disable' );
142 if ( isset( $filterDefinition[
'showHideSuffix'] ) ) {
143 $filterDefinition[
'showHide'] =
'wl' . $filterDefinition[
'showHideSuffix'];
146 return $filterDefinition;
154 parent::registerFilters();
158 'name' =>
'extended-group',
161 'name' =>
'extended',
162 'isReplacedInStructuredUi' =>
true,
163 'activeValue' =>
false,
164 'default' => $this->
getUser()->getBoolOption(
'extendwatchlist' ),
165 'queryCallable' =>
function ( $specialClassName, $ctx,
$dbr, &$tables,
166 &$fields, &$conds, &$query_options, &$join_conds ) {
167 $nonRevisionTypes = [
RC_LOG ];
168 Hooks::run(
'SpecialWatchlistGetNonRevisionTypes', [ &$nonRevisionTypes ] );
169 if ( $nonRevisionTypes ) {
170 $conds[] =
$dbr->makeList(
172 'rc_this_oldid=page_latest',
173 'rc_type' => $nonRevisionTypes,
186 ->getFilter(
'hidepreviousrevisions' )
187 ->setDefault( !$this->
getUser()->getBoolOption(
'extendwatchlist' ) );
191 'name' =>
'watchlistactivity',
192 'title' =>
'rcfilters-filtergroup-watchlistactivity',
193 'class' => ChangesListStringOptionsFilterGroup::class,
195 'isFullCoverage' =>
true,
199 'label' =>
'rcfilters-filter-watchlistactivity-unseen-label',
200 'description' =>
'rcfilters-filter-watchlistactivity-unseen-description',
201 'cssClassSuffix' =>
'watchedunseen',
202 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
208 'label' =>
'rcfilters-filter-watchlistactivity-seen-label',
209 'description' =>
'rcfilters-filter-watchlistactivity-seen-description',
210 'cssClassSuffix' =>
'watchedseen',
211 'isRowApplicableCallable' =>
function ( $ctx,
RecentChange $rc ) {
217 'queryCallable' =>
function (
218 $specialPageClassName,
228 if ( $selectedValues === [
'seen' ] ) {
229 $conds[] =
$dbr->makeList( [
230 'wl_notificationtimestamp IS NULL',
231 'rc_timestamp < wl_notificationtimestamp'
233 } elseif ( $selectedValues === [
'unseen' ] ) {
234 $conds[] =
$dbr->makeList( [
235 'wl_notificationtimestamp IS NOT NULL',
236 'rc_timestamp >= wl_notificationtimestamp'
245 $hideMinor = $significance->getFilter(
'hideminor' );
246 $hideMinor->setDefault( $user->getBoolOption(
'watchlisthideminor' ) );
249 $hideBots = $automated->getFilter(
'hidebots' );
250 $hideBots->setDefault( $user->getBoolOption(
'watchlisthidebots' ) );
253 $hideAnons = $registration->getFilter(
'hideanons' );
254 $hideAnons->setDefault( $user->getBoolOption(
'watchlisthideanons' ) );
255 $hideLiu = $registration->getFilter(
'hideliu' );
256 $hideLiu->setDefault( $user->getBoolOption(
'watchlisthideliu' ) );
260 if ( $user->getBoolOption(
'watchlisthideanons' ) &&
261 !$user->getBoolOption(
'watchlisthideliu' )
264 ->setDefault(
'registered' );
267 if ( $user->getBoolOption(
'watchlisthideliu' ) &&
268 !$user->getBoolOption(
'watchlisthideanons' )
271 ->setDefault(
'unregistered' );
275 if ( $reviewStatus !==
null ) {
277 if ( $user->getBoolOption(
'watchlisthidepatrolled' ) ) {
278 $reviewStatus->setDefault(
'unpatrolled' );
279 $legacyReviewStatus = $this->
getFilterGroup(
'legacyReviewStatus' );
280 $legacyHidePatrolled = $legacyReviewStatus->getFilter(
'hidepatrolled' );
281 $legacyHidePatrolled->setDefault(
true );
286 $hideMyself = $authorship->getFilter(
'hidemyself' );
287 $hideMyself->setDefault( $user->getBoolOption(
'watchlisthideown' ) );
290 $hideCategorization = $changeType->getFilter(
'hidecategorization' );
291 if ( $hideCategorization !==
null ) {
293 $hideCategorization->setDefault( $user->getBoolOption(
'watchlisthidecategorization' ) );
307 static $compatibilityMap = [
308 'hideMinor' =>
'hideminor',
309 'hideBots' =>
'hidebots',
310 'hideAnons' =>
'hideanons',
311 'hideLiu' =>
'hideliu',
312 'hidePatrolled' =>
'hidepatrolled',
313 'hideOwn' =>
'hidemyself',
317 foreach ( $compatibilityMap as $from => $to ) {
318 if ( isset( $params[$from] ) ) {
319 $params[$to] = $params[$from];
320 unset( $params[$from] );
324 if ( $this->
getRequest()->getVal(
'action' ) ==
'submit' ) {
325 $allBooleansFalse = [];
334 $allBooleansFalse[
$filter->getName() ] =
false;
337 $params = $params + $allBooleansFalse;
343 $opts->fetchValuesFromRequest( $request );
351 protected function doMainQuery( $tables, $fields, $conds, $query_options,
358 $tables = array_merge( $tables, $rcQuery[
'tables'], [
'watchlist' ] );
359 $fields = array_merge( $rcQuery[
'fields'], $fields );
361 $join_conds = array_merge(
366 'wl_user' => $user->getId(),
367 'wl_namespace=rc_namespace',
377 $fields[] =
'page_latest';
378 $join_conds[
'page'] = [
'LEFT JOIN',
'rc_cur_id=page_id' ];
380 $fields[] =
'wl_notificationtimestamp';
384 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
385 if ( !$permissionManager->userHasRight( $user,
'deletedhistory' ) ) {
387 } elseif ( !$permissionManager->userHasAnyRight( $user,
'suppressrevision',
'viewsuppressed' ) ) {
393 $conds[] =
$dbr->makeList( [
395 $dbr->bitAnd(
'rc_deleted', $bitmask ) .
" != $bitmask",
399 $tagFilter = $opts[
'tagfilter'] ? explode(
'|', $opts[
'tagfilter'] ) : [];
409 $this->
runMainQueryHook( $tables, $fields, $conds, $query_options, $join_conds, $opts );
416 'ORDER BY' =>
'rc_timestamp DESC',
417 'LIMIT' => $opts[
'limit']
419 if ( in_array(
'DISTINCT', $query_options ) ) {
425 $orderByAndLimit[
'ORDER BY'] =
'rc_timestamp DESC, rc_id DESC';
426 $orderByAndLimit[
'GROUP BY'] =
'rc_timestamp, rc_id';
430 $query_options = array_merge( $orderByAndLimit, $query_options );
456 $wlToken = $user->getTokenFromOption(
'watchlisttoken' );
459 'action' =>
'feedwatchlist',
461 'wlowner' => $user->getName(),
462 'wltoken' => $wlToken,
477 $services = MediaWikiServices::getInstance();
479 # Show a message about replica DB lag, if applicable
480 $lag =
$dbr->getSessionLagStatus()[
'lag'];
482 $output->showLagWarning( $lag );
485 # If no rows to display, show message before try to render the list
486 if ( $rows->numRows() == 0 ) {
488 "<div class='mw-changeslist-empty'>\n$1\n</div>",
'recentchanges-noresult'
493 $dbr->dataSeek( $rows, 0 );
496 $list->setWatchlistDivs();
497 $list->initChangesListRows( $rows );
498 if ( $user->getOption(
'watchlistunwatchlinks' ) ) {
506 return $this->getLinkRenderer()
507 ->makeKnownLink( $rc->getTitle(),
508 $this->msg(
'watchlist-unwatch' )->text(), [
509 'class' =>
'mw-unwatch-link',
510 'title' => $this->msg(
'tooltip-ca-unwatch' )->text()
511 ], [
'action' =>
'unwatch' ] ) .
"\u{00A0}";
515 $dbr->dataSeek( $rows, 0 );
517 if ( $this->
getConfig()->
get(
'RCShowWatchingUsers' )
518 && $user->getOption(
'shownumberswatching' )
520 $watchedItemStore = $services->getWatchedItemStore();
523 $s = $list->beginRecentChangesList();
529 $userShowHiddenCats = $this->
getUser()->getBoolOption(
'showhiddencats' );
531 foreach ( $rows as $obj ) {
535 # Skip CatWatch entries for hidden cats based on user preference
538 !$userShowHiddenCats &&
539 $rc->getParam(
'hidden-cat' )
544 $rc->counter = $counter++;
546 if ( $this->
getConfig()->get(
'ShowUpdatedMarker' ) ) {
552 if ( isset( $watchedItemStore ) ) {
553 $rcTitleValue =
new TitleValue( (
int)$obj->rc_namespace, $obj->rc_title );
554 $rc->numberofWatchingusers = $watchedItemStore->countWatchers( $rcTitleValue );
556 $rc->numberofWatchingusers = 0;
562 $changeLine = $list->recentChangesLine( $rc, $unseen, $counter );
563 if ( $changeLine !==
false ) {
567 $s .= $list->endRecentChangesList();
583 $this->
msg(
'watchlistfor2', $user->getName() )
597 'id' =>
'mw-watchlist-form'
599 $form .= Html::hidden(
'title', $this->
getPageTitle()->getPrefixedText() );
602 [
'id' =>
'mw-watchlist-options',
'class' =>
'cloptions' ]
605 'legend',
null, $this->
msg(
'watchlist-options' )->text()
614 $now =
$lang->userTimeAndDate( $timestamp, $user );
615 $wlInfo = Html::rawElement(
619 'data-params' => json_encode( [
'from' => $timestamp,
'fromFormatted' => $now ] ),
621 $this->
msg(
'wlnote' )->numParams( $numRows, round( $opts[
'days'] * 24 ) )->params(
622 $lang->userDate( $timestamp, $user ),
$lang->userTime( $timestamp, $user )
626 $nondefaults = $opts->getChangedValues();
627 $cutofflinks = Html::rawElement(
629 [
'class' =>
'cldays cloption' ],
633 # Spit out some control panel links
635 $namesOfDisplayedFilters = [];
637 $namesOfDisplayedFilters[] = $filterName;
642 $opts[ $filterName ],
643 $filter->isFeatureAvailableOnStructuredUi( $this )
647 $hiddenFields = $nondefaults;
648 $hiddenFields[
'action'] =
'submit';
649 unset( $hiddenFields[
'namespace'] );
650 unset( $hiddenFields[
'invert'] );
651 unset( $hiddenFields[
'associated'] );
652 unset( $hiddenFields[
'days'] );
653 foreach ( $namesOfDisplayedFilters as $filterName ) {
654 unset( $hiddenFields[$filterName] );
657 # Namespace filter and put the whole form together.
659 $form .= $cutofflinks;
660 $form .= Html::rawElement(
662 [
'class' =>
'clshowhide' ],
663 $this->
msg(
'watchlist-hide' ) .
664 $this->
msg(
'colon-separator' )->escaped() .
665 implode(
' ', $links )
667 $form .=
"\n<br />\n";
669 $namespaceForm = Html::namespaceSelector(
671 'selected' => $opts[
'namespace'],
673 'label' => $this->
msg(
'namespace' )->text(),
674 'in-user-lang' =>
true,
676 'name' =>
'namespace',
678 'class' =>
'namespaceselector',
681 $hidden = $opts[
'namespace'] ===
'' ?
' mw-input-hidden' :
'';
682 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' .
Xml::checkLabel(
683 $this->
msg(
'invert' )->text(),
687 [
'title' => $this->
msg(
'tooltip-invert' )->text() ]
689 $namespaceForm .=
'<span class="mw-input-with-label' . $hidden .
'">' .
Xml::checkLabel(
690 $this->
msg(
'namespace_association' )->text(),
694 [
'title' => $this->
msg(
'tooltip-namespace_association' )->text() ]
696 $form .= Html::rawElement(
698 [
'class' =>
'namespaceForm cloption' ],
703 $this->
msg(
'watchlist-submit' )->text(),
704 [
'class' =>
'cloption-submit' ]
706 foreach ( $hiddenFields as $key => $value ) {
707 $form .= Html::hidden( $key, $value ) .
"\n";
714 $rcfilterContainer = Html::element(
717 [
'class' =>
'rcfilters-container mw-rcfilters-container' ]
720 $loadingContainer = Html::rawElement(
722 [
'class' =>
'mw-rcfilters-spinner' ],
725 [
'class' =>
'mw-rcfilters-spinner-bounce' ]
734 [
'class' =>
'rcfilters-head mw-rcfilters-head' ],
735 $rcfilterContainer . $form
740 $this->
getOutput()->addHTML( $loadingContainer );
749 $selected = (float)$options[
'days'];
750 if ( $selected <= 0 ) {
754 $selectedHours = round( $selected * 24 );
756 $hours = array_unique( array_filter( [
764 24 * (
float)$this->
getUser()->getOption(
'watchlistdays', 0 ),
770 $select =
new XmlSelect(
'days',
'days', (
float)( $selectedHours / 24 ) );
772 foreach ( $hours as $value ) {
774 $name = $this->
msg(
'hours' )->numParams( $value )->text();
776 $name = $this->
msg(
'days' )->numParams( $value / 24 )->text();
778 $select->addOption( $name, (
float)( $value / 24 ) );
781 return $select->getHTML() .
"\n<br />\n";
790 $showUpdatedMarker = $this->
getConfig()->get(
'ShowUpdatedMarker' );
793 $watchlistHeader =
'';
794 if ( $numItems == 0 ) {
795 $watchlistHeader = $this->
msg(
'nowatchlist' )->parse();
797 $watchlistHeader .= $this->
msg(
'watchlist-details' )->numParams( $numItems )->parse() .
"\n";
798 if ( $this->
getConfig()->
get(
'EnotifWatchlist' )
799 && $user->getOption(
'enotifwatchlistpages' )
801 $watchlistHeader .= $this->
msg(
'wlheader-enotif' )->parse() .
"\n";
803 if ( $showUpdatedMarker ) {
804 $watchlistHeader .= $this->
msg(
806 'rcfilters-watchlist-showupdated' :
807 'wlheader-showupdated'
811 $form .= Html::rawElement(
813 [
'class' =>
'watchlistDetails' ],
817 if ( $numItems > 0 && $showUpdatedMarker ) {
820 'id' =>
'mw-watchlist-resetbutton' ] ) .
"\n" .
822 [
'name' =>
'mw-watchlist-reset-submit' ] ) .
"\n" .
823 Html::hidden(
'token', $user->getEditToken() ) .
"\n" .
824 Html::hidden(
'reset',
'all' ) .
"\n";
825 foreach ( $nondefaults as $key => $value ) {
826 $form .= Html::hidden( $key, $value ) .
"\n";
834 protected function showHideCheck( $options, $message, $name, $value, $inStructuredUi ) {
835 $options[$name] = 1 - (int)$value;
837 $attribs = [
'class' =>
'mw-input-with-label clshowhideoption cloption' ];
838 if ( $inStructuredUi ) {
839 $attribs[
'data-feature-in-structured-ui' ] =
true;
842 return Html::rawElement(
846 Html::check( $name, (
int)$value, [
'id' => $name ] ) . Html::rawElement(
848 $attribs + [
'for' => $name ],
850 $this->
msg( $message,
'<nowiki/>' )->parse()
863 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
864 $count = $store->countWatchedItems( $this->
getUser() );
865 return floor( $count / 2 );
875 return ( $firstUnseen ===
null || $firstUnseen > $rc->
getAttribute(
'rc_timestamp' ) );
883 return $this->watchStore->getLatestNotificationTimestamp(