Merge "Add clock icon to expiring items in Special:Watchlist"
This commit is contained in:
commit
da6f6a2aa8
12 changed files with 162 additions and 2 deletions
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ class RCCacheEntry extends RecentChange {
|
|||
public $usertalklink;
|
||||
/** @var bool|null */
|
||||
public $watched;
|
||||
/** @var string|null */
|
||||
public $watchlistExpiry;
|
||||
|
||||
/**
|
||||
* @param RecentChange $rc
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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() ?
|
||||
|
|
|
|||
|
|
@ -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() );
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -2183,6 +2183,7 @@ return [
|
|||
'mediawiki.Title',
|
||||
'mediawiki.util',
|
||||
'oojs-ui-core',
|
||||
'oojs-ui.styles.icons-interactions',
|
||||
'user.options',
|
||||
],
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue