Merge "Add clock icon to expiring items in Special:Watchlist"

This commit is contained in:
jenkins-bot 2020-06-24 17:34:50 +00:00 committed by Gerrit Code Review
commit da6f6a2aa8
12 changed files with 162 additions and 2 deletions

View file

@ -28,6 +28,7 @@ use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\User\UserIdentityValue;
use OOUI\IconWidget;
use Wikimedia\Rdbms\IResultWrapper;
class ChangesList extends ContextSource {
@ -538,6 +539,9 @@ class ChangesList extends ContextSource {
}
/**
* Get the HTML link to the changed page, possibly with a prefix from hook handlers, and a
* suffix for temporarily watched items.
*
* @param RecentChange &$rc
* @param bool $unpatrolled
* @param bool $watched
@ -569,7 +573,43 @@ class ChangesList extends ContextSource {
$this->getHookRunner()->onChangesListInsertArticleLink( $this, $articlelink,
$s, $rc, $unpatrolled, $watched );
return "{$s} {$articlelink}";
// Watchlist expiry icon.
$watchlistExpiry = '';
if ( isset( $rc->watchlistExpiry ) && $rc->watchlistExpiry ) {
$watchlistExpiry = $this->getWatchlistExpiry( $rc );
}
return "{$s} {$articlelink}{$watchlistExpiry}";
}
/**
* Get HTML to display the clock icon for watched items that have a watchlist expiry time.
* @since 1.35
* @param RecentChange $recentChange
* @return string The HTML to display an indication of the expiry time.
*/
public function getWatchlistExpiry( RecentChange $recentChange ): string {
$item = WatchedItem::newFromRecentChange( $recentChange, $this->getUser() );
// Guard against expired items, even though they shouldn't come here.
if ( $item->isExpired() ) {
return '';
}
$daysLeft = $item->getExpiryInDays();
$daysLeftMsg = $this->msg( 'watchlist-expires-in', $daysLeft );
// Matching widget is also created in ChangesListSpecialPage, for the legend.
$widget = new IconWidget( [
'icon' => 'clock',
'title' => $daysLeftMsg->text(),
'classes' => [ 'mw-changesList-watchlistExpiry' ],
] );
// Add labels for assistive technologies.
$widget->setAttributes( [
'role' => 'img',
'aria-label' => $this->msg( 'watchlist-expires-in-aria-label' )->text(),
] );
// Add spaces around the widget (the page title is to one side,
// and a semicolon or opening-parenthesis to the other).
return " $widget ";
}
/**
@ -581,12 +621,15 @@ class ChangesList extends ContextSource {
* @return string HTML fragment
*/
public function getTimestamp( $rc ) {
// This uses the semi-colon separator unless there's a watchlist expiry date for the entry,
// because in that case the timestamp is preceeded by a clock icon.
// A space is important after mw-changeslist-separator--semicolon to make sure
// that whatever comes before it is distinguishable.
// (Otherwise your have the text of titles pushing up against the timestamp)
// A specific element is used for this purpose as `mw-changeslist-date` is used in a variety
// of other places with a different position and the information proceeding getTimestamp can vary.
return '<span class="mw-changeslist-separator--semicolon"></span> ' .
$separatorClass = $rc->watchlistExpiry ? 'mw-changeslist-separator' : 'mw-changeslist-separator--semicolon';
return Html::element( 'span', [ 'class' => $separatorClass ] ) . ' ' .
'<span class="mw-changeslist-date">' .
htmlspecialchars( $this->getLanguage()->userTime(
$rc->mAttribs['rc_timestamp'],

View file

@ -37,6 +37,8 @@ class RCCacheEntry extends RecentChange {
public $usertalklink;
/** @var bool|null */
public $watched;
/** @var string|null */
public $watchlistExpiry;
/**
* @param RecentChange $rc

View file

@ -64,6 +64,7 @@ class RCCacheEntryFactory {
$cacheEntry->watched = $cacheEntry->mAttribs['rc_type'] == RC_LOG ? false : $watched;
$cacheEntry->numberofWatchingusers = $baseRC->numberofWatchingusers;
$cacheEntry->watchlistExpiry = $baseRC->watchlistExpiry;
$cacheEntry->link = $this->buildCLink( $cacheEntry );
$cacheEntry->timestamp = $this->buildTimestamp( $cacheEntry );

View file

@ -63,6 +63,7 @@ use Wikimedia\IPUtils;
* temporary: not stored in the database
* notificationtimestamp
* numberofWatchingusers
* watchlistExpiry for temporary watchlist items
*
* @todo Deprecate access to mAttribs (direct or via getAttributes). Right now
* we're having to include both rc_comment and rc_comment_text/rc_comment_data
@ -108,6 +109,11 @@ class RecentChange implements Taggable {
public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
public $notificationtimestamp;
/**
* @var string|null The expiry time, if this is a temporary watchlist item.
*/
public $watchlistExpiry;
/**
* @var int Line number of recent change. Default -1.
*/
@ -1022,6 +1028,11 @@ class RecentChange implements Taggable {
$this->mAttribs['rc_user'] = $user->getId();
$this->mAttribs['rc_user_text'] = $user->getName();
$this->mAttribs['rc_actor'] = $user->getActorId();
// Watchlist expiry.
if ( isset( $row->we_expiry ) && $row->we_expiry ) {
$this->watchlistExpiry = wfTimestamp( TS_MW, $row->we_expiry );
}
}
/**

View file

@ -23,6 +23,7 @@
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use OOUI\IconWidget;
use Wikimedia\Rdbms\DBQueryTimeoutError;
use Wikimedia\Rdbms\FakeResultWrapper;
use Wikimedia\Rdbms\IDatabase;
@ -1761,6 +1762,29 @@ abstract class ChangesListSpecialPage extends SpecialPage {
[ 'class' => 'mw-changeslist-legend-plusminus' ],
$context->msg( 'recentchanges-label-plusminus' )->text()
) . "\n";
// Watchlist expiry clock icon.
if ( $this->getName() === 'Watchlist' && $context->getConfig()->get( 'WatchlistExpiry' ) ) {
$widget = new IconWidget( [
'icon' => 'clock',
'classes' => [ 'mw-changesList-watchlistExpiry' ],
] );
// Link the image to its label for assistive technologies.
$watchlistLabelId = 'mw-changeslist-watchlistExpiry-label';
$widget->getIconElement()->setAttributes( [
'role' => 'img',
'aria-labelledby' => $watchlistLabelId,
] );
$legend .= Html::rawElement(
'dt',
[ 'class' => 'mw-changeslist-legend-watchlistexpiry' ],
$widget
);
$legend .= Html::rawElement(
'dd',
[ 'class' => 'mw-changeslist-legend-watchlistexpiry', 'id' => $watchlistLabelId ],
$context->msg( 'recentchanges-legend-watchlistexpiry' )->text()
);
}
$legend .= Html::closeElement( 'dl' ) . "\n";
$legendHeading = $this->isStructuredFilterUiEnabled() ?

View file

@ -106,6 +106,11 @@ class SpecialWatchlist extends ChangesListSpecialPage {
return;
}
// Enable OOUI for the clock icon.
if ( $config->get( 'WatchlistExpiry' ) ) {
$output->enableOOUI();
}
parent::execute( $subpage );
if ( $this->isStructuredFilterUiEnabled() ) {
@ -379,6 +384,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
if ( $this->getConfig()->get( 'WatchlistExpiry' ) ) {
$tables[] = 'watchlist_expiry';
$fields[] = 'we_expiry';
$join_conds['watchlist_expiry'] = [ 'LEFT JOIN', 'wl_id = we_item' ];
$conds[] = 'we_expiry IS NULL OR we_expiry > ' . $dbr->addQuotes( $dbr->timestamp() );
}

View file

@ -78,6 +78,21 @@ class WatchedItem {
$this->expiry = $expiry;
}
/**
* @since 1.35
* @param RecentChange $recentChange
* @param UserIdentity $user
* @return WatchedItem
*/
public static function newFromRecentChange( RecentChange $recentChange, UserIdentity $user ) {
return new self(
$user,
$recentChange->getTitle(),
$recentChange->notificationtimestamp,
$recentChange->watchlistExpiry
);
}
/**
* @deprecated since 1.34, use getUserIdentity()
* @return User

View file

@ -1448,6 +1448,7 @@
"recentchanges-legend-bot": "{{int:recentchanges-label-bot}}",
"recentchanges-legend-unpatrolled": "{{int:recentchanges-label-unpatrolled}}",
"recentchanges-legend-plusminus": "(<em>±123</em>)",
"recentchanges-legend-watchlistexpiry": "Temporarily watched page",
"recentchanges-submit": "Show",
"rcfilters-tag-remove": "Remove '$1'",
"rcfilters-legend-heading": "<strong>List of abbreviations:</strong>",
@ -3385,6 +3386,8 @@
"confirm-watch-label": "Watchlist time period:",
"watchlist-expiry-options": "Permanently:infinite,1 week:1 week,1 month:1 month,3 months:3 months,6 months:6 months",
"watchlist-expiry-days-left": "{{PLURAL:$1|$1 day|$1 days}} left",
"watchlist-expires-in": "{{PLURAL:$1|$1 day|$1 days}} left in your watchlist",
"watchlist-expires-in-aria-label": "Expiring watchlist item",
"confirm-watch-button-expiry": "Watch",
"confirm-unwatch-button": "OK",
"confirm-unwatch-top": "Remove this page from your watchlist?",

View file

@ -1663,6 +1663,7 @@
"recentchanges-legend-bot": "Used as legend on [[Special:RecentChanges]] and [[Special:Watchlist]].\n\nRefers to {{msg-mw|Recentchanges-label-bot}}.",
"recentchanges-legend-unpatrolled": "Used as legend on [[Special:RecentChanges]] and [[Special:Watchlist]].\n\nRefers to {{msg-mw|Recentchanges-label-unpatrolled}}.",
"recentchanges-legend-plusminus": "{{optional}}\nA plus/minus sign with a number for the legend.",
"recentchanges-legend-watchlistexpiry": "Used as legend on [[Special:Watchlist]], next to an icon of a clock indicating that a watchlist item is temporarily watched.",
"recentchanges-submit": "Label for submit button in [[Special:RecentChanges]]\n{{Identical|Show}}",
"rcfilters-tag-remove": "A tooltip for the button that removes a filter from the active filters area in [[Special:RecentChanges]] and [[Special:Watchlist]] when RCFilters are enabled. \n\nParameters: $1 - Tag label\n{{Identical|Remove}}",
"rcfilters-legend-heading": "Used as a heading for legend box on [[Special:RecentChanges]] and [[Special:Watchlist]] when RCFilters are enabled.",
@ -3600,6 +3601,8 @@
"confirm-watch-label": "Label for select list of watchlist expiry options.",
"watchlist-expiry-options": "Options for the expiry of watchlist items.\n\n{{doc-mediawiki-options-list}}",
"watchlist-expiry-days-left": "Select-list option used to display the remaining number of days for a watchlist item that's due to expire.\n\nParemeter:\n* $1 - the integer number of days.",
"watchlist-expires-in": "Tooltip for clock icon in Special:Watchlist shown on expiring items.\n\nParemeter:\n* $1 - the integer number of days.",
"watchlist-expires-in-aria-label": "ARIA label for the clock icon in expiring entries in Special:Watchlist.",
"confirm-watch-button-expiry": "Used as Submit button text.",
"confirm-unwatch-button": "Used as Submit button text.\n{{Identical|OK}}",
"confirm-unwatch-top": "Used as confirmation message.",

View file

@ -2183,6 +2183,7 @@ return [
'mediawiki.Title',
'mediawiki.util',
'oojs-ui-core',
'oojs-ui.styles.icons-interactions',
'user.options',
],
],

View file

@ -13,3 +13,12 @@ span.mw-changeslist-line-prefix {
.mw-changeslist-line-prefix {
width: 1.25em;
}
/* Make the clock icon smaller than it is by default, and grey it out. */
.mw-changesList-watchlistExpiry.oo-ui-iconElement-icon {
min-height: 13px;
height: 13px;
position: relative;
top: -1px;
opacity: 0.51; /* To match @opacity-base--disabled */
}

View file

@ -249,4 +249,46 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
return $method->invokeArgs( $enhancedChangesList, [ $cacheEntry ] );
}
public function testExpiringWatchlistItem(): void {
// Set current time to 2020-05-05.
MWTimestamp::setFakeTime( '20200505000000' );
$enhancedChangesList = $this->newEnhancedChangesList();
$enhancedChangesList->getOutput()->enableOOUI();
$row = (object)[
'rc_namespace' => NS_MAIN,
'rc_title' => '',
'rc_timestamp' => '20150921134808',
'rc_deleted' => '',
'rc_comment_text' => 'comment',
'rc_comment_data' => null,
'rc_user' => $this->getTestUser()->getUser()->getId(),
'we_expiry' => '20200101000000',
];
$rc = RecentChange::newFromRow( $row );
// Make sure it doesn't output anything for a past expiry.
$html1 = $enhancedChangesList->getWatchlistExpiry( $rc );
$this->assertSame( '', $html1 );
// Check a future expiry for the right tooltip text.
$rc->watchlistExpiry = '20200512000000';
$html2 = $enhancedChangesList->getWatchlistExpiry( $rc );
$this->assertStringContainsString( "title='7 days left in your watchlist'", $html2 );
// Check that multiple changes on the same day all get the clock icon.
$enhancedChangesList->beginRecentChangesList();
// 1. Expire on 2020-06-01 (27 days):
$rc1 = $this->getEditChange( '20200501000001', __METHOD__ . '1' );
$rc1->watchlistExpiry = '20200601000000';
$enhancedChangesList->recentChangesLine( $rc1 );
// 2. Expire on 2020-06-08 (34 days):
$rc2 = $this->getEditChange( '20200501000002', __METHOD__ . '2' );
$rc2->watchlistExpiry = '20200608000000';
$enhancedChangesList->recentChangesLine( $rc2 );
// Get and test the HTML.
$html3 = $enhancedChangesList->endRecentChangesList();
$this->assertStringContainsString( '27 days left in your watchlist', $html3 );
$this->assertStringContainsString( '34 days left in your watchlist', $html3 );
}
}