Unwatch link for pages in Special:Watchlist
authorGeoffrey Mon <geofbot@gmail.com>
Sat, 11 Jun 2016 00:59:58 +0000 (20:59 -0400)
committerBartosz Dziewoński <matma.rex@gmail.com>
Sat, 12 Aug 2017 23:36:29 +0000 (19:36 -0400)
When the 'watchlistunwatchlinks' preference option is enabled, this
adds a '×' link to each entry of the watchlist that unwatches the page
of that entry. When clicked, it changes into a '+' which can be used to
re-watch the page (effectively undoing the earlier unwatch).
When a page is unwatched, its entries and the entries of its associated
talk page (or vice versa) become translucent and are struck through.

Without JS, '×'/'+' link to action=(un)watch for the relevant page.

In addition, ChangesList classes have been modified to allow a prefixer
that adds a prefix to each line (used in this case to put the unwatch
link) and to add HTML data attributes to reliably determine the target
page of each entry. Unit tests have been updated accordingly.

Bug: T2424
Change-Id: I450b2901413d7e75c11de2a446829fdbb22d31e1

16 files changed:
RELEASE-NOTES-1.30
includes/DefaultSettings.php
includes/Preferences.php
includes/changes/ChangesList.php
includes/changes/EnhancedChangesList.php
includes/changes/OldChangesList.php
includes/specials/SpecialWatchlist.php
includes/templates/EnhancedChangesListGroup.mustache
languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki.special/mediawiki.special.watchlist.css [new file with mode: 0644]
resources/src/mediawiki.special/mediawiki.special.watchlist.js
tests/phpunit/includes/changes/EnhancedChangesListTest.php
tests/phpunit/includes/changes/OldChangesListTest.php
tests/phpunit/includes/specials/SpecialWatchlistTest.php

index 452cb35..9c4bcf0 100644 (file)
@@ -49,6 +49,10 @@ section).
 * Added RecentChangesPurgeRows hook to allow extensions to purge data that
   depends on the recentchanges table.
 * Added JS config values wgDiffOldId/wgDiffNewId to the output of diff pages.
+* (T2424) Added direct unwatch links to entries in Special:Watchlist (if the
+  'watchlistunwatchlinks' preference option is enabled). With JavaScript
+  enabled, these links toggle so the user can also re-watch pages that have
+  just been unwatched.
 
 === Languages updated in 1.30 ===
 
index ba755fa..7fa55bc 100644 (file)
@@ -4933,6 +4933,7 @@ $wgDefaultUserOptions = [
        'watchlisthidepatrolled' => 0,
        'watchlisthidecategorization' => 1,
        'watchlistreloadautomatically' => 0,
+       'watchlistunwatchlinks' => 0,
        'watchmoves' => 0,
        'watchrollback' => 0,
        'wllimit' => 250,
index 039d99c..c74d6e1 100644 (file)
@@ -1044,6 +1044,11 @@ class Preferences {
                        'section' => 'watchlist/advancedwatchlist',
                        'label-message' => 'tog-watchlistreloadautomatically',
                ];
+               $defaultPreferences['watchlistunwatchlinks'] = [
+                       'type' => 'toggle',
+                       'section' => 'watchlist/advancedwatchlist',
+                       'label-message' => 'tog-watchlistunwatchlinks',
+               ];
 
                if ( $config->get( 'RCWatchCategoryMembership' ) ) {
                        $defaultPreferences['watchlisthidecategorization'] = [
index 65eb320..cac4769 100644 (file)
@@ -41,6 +41,9 @@ class ChangesList extends ContextSource {
        protected $rclistOpen;
        protected $rcMoveIndex;
 
+       /** @var callable */
+       protected $changeLinePrefixer;
+
        /** @var BagOStuff */
        protected $watchMsgCache;
 
@@ -169,12 +172,14 @@ class ChangesList extends ContextSource {
         * @return array of classes
         */
        protected function getHTMLClasses( $rc, $watched ) {
-               $classes = [];
+               $classes = [ self::CSS_CLASS_PREFIX . 'line' ];
                $logType = $rc->mAttribs['rc_log_type'];
 
                if ( $logType ) {
+                       $classes[] = self::CSS_CLASS_PREFIX . 'log';
                        $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'log-' . $logType );
                } else {
+                       $classes[] = self::CSS_CLASS_PREFIX . 'edit';
                        $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns' .
                                $rc->mAttribs['rc_namespace'] . '-' . $rc->mAttribs['rc_title'] );
                        $classes[] = Sanitizer::escapeClass( self::CSS_CLASS_PREFIX . 'ns-' .
@@ -766,4 +771,15 @@ class ChangesList extends ContextSource {
 
                return $attrs;
        }
+
+       /**
+        * Sets the callable that generates a change line prefix added to the beginning of each line.
+        *
+        * @param callable $prefixer Callable to run that generates the change line prefix.
+        *     Takes three parameters: a RecentChange object, a ChangesList object,
+        *     and whether the current entry is a grouped entry.
+        */
+       public function setChangeLinePrefixer( callable $prefixer ) {
+               $this->changeLinePrefixer = $prefixer;
+       }
 }
index ffe668c..def6457 100644 (file)
@@ -172,12 +172,14 @@ class EnhancedChangesList extends ChangesList {
                $recentChangesFlags = $this->getConfig()->get( 'RecentChangesFlags' );
 
                # Add the namespace and title of the block as part of the class
-               $tableClasses = [ 'mw-collapsible', 'mw-collapsed', 'mw-enhanced-rc' ];
+               $tableClasses = [ 'mw-collapsible', 'mw-collapsed', 'mw-enhanced-rc', 'mw-changeslist-line' ];
                if ( $block[0]->mAttribs['rc_log_type'] ) {
                        # Log entry
+                       $tableClasses[] = 'mw-changeslist-log';
                        $tableClasses[] = Sanitizer::escapeClass( 'mw-changeslist-log-'
                                . $block[0]->mAttribs['rc_log_type'] );
                } else {
+                       $tableClasses[] = 'mw-changeslist-edit';
                        $tableClasses[] = Sanitizer::escapeClass( 'mw-changeslist-ns'
                                . $block[0]->mAttribs['rc_namespace'] . '-' . $block[0]->mAttribs['rc_title'] );
                }
@@ -330,6 +332,11 @@ class EnhancedChangesList extends ChangesList {
                        implode( $this->message['semicolon-separator'], $users )
                )->escaped();
 
+               $prefix = '';
+               if ( is_callable( $this->changeLinePrefixer ) ) {
+                       $prefix = call_user_func( $this->changeLinePrefixer, $block[0], $this, true );
+               }
+
                $templateParams = [
                        'articleLink' => $articleLink,
                        'charDifference' => $charDifference,
@@ -338,6 +345,7 @@ class EnhancedChangesList extends ChangesList {
                        'lines' => $lines,
                        'logText' => $logText,
                        'numberofWatchingusers' => $numberofWatchingusers,
+                       'prefix' => $prefix,
                        'rev-deleted-event' => $revDeletedMsg,
                        'tableClasses' => $tableClasses,
                        'timestamp' => $block[0]->timestamp,
@@ -366,7 +374,7 @@ class EnhancedChangesList extends ChangesList {
 
                $type = $rcObj->mAttribs['rc_type'];
                $data = [];
-               $lineParams = [];
+               $lineParams = [ 'targetTitle' => $rcObj->getTitle() ];
 
                $classes = [ 'mw-enhanced-rc' ];
                if ( $rcObj->watched
@@ -609,8 +617,10 @@ class EnhancedChangesList extends ChangesList {
 
                if ( $logType ) {
                        # Log entry
+                       $classes[] = 'mw-changeslist-log';
                        $classes[] = Sanitizer::escapeClass( 'mw-changeslist-log-' . $logType );
                } else {
+                       $classes[] = 'mw-changeslist-edit';
                        $classes[] = Sanitizer::escapeClass( 'mw-changeslist-ns' .
                                $rcObj->mAttribs['rc_namespace'] . '-' . $rcObj->mAttribs['rc_title'] );
                }
@@ -690,8 +700,15 @@ class EnhancedChangesList extends ChangesList {
                        return $key === 'class' || Sanitizer::isReservedDataAttribute( $key );
                } );
 
+               $prefix = '';
+               if ( is_callable( $this->changeLinePrefixer ) ) {
+                       $prefix = call_user_func( $this->changeLinePrefixer, $rcObj, $this, false );
+               }
+
                $line = Html::openElement( 'table', $attribs ) . Html::openElement( 'tr' );
-               $line .= '<td class="mw-enhanced-rc"><span class="mw-enhancedchanges-arrow-space"></span>';
+               $line .= Html::rawElement( 'td', [], '<span class="mw-enhancedchanges-arrow-space"></span>' );
+               $line .= Html::rawElement( 'td', [ 'class' => 'mw-changeslist-line-prefix' ], $prefix );
+               $line .= '<td class="mw-enhanced-rc">';
 
                if ( isset( $data['recentChangesFlags'] ) ) {
                        $line .= $this->recentChangesFlags( $data['recentChangesFlags'] );
@@ -702,7 +719,12 @@ class EnhancedChangesList extends ChangesList {
                        $line .= '&#160;' . $data['timestampLink'];
                        unset( $data['timestampLink'] );
                }
-               $line .= '&#160;</td><td>';
+               $line .= '&#160;</td>';
+               $line .= Html::openElement( 'td', [
+                       'class' => 'mw-changeslist-line-inner',
+                       // Used for reliable determination of the affiliated page
+                       'data-target-page' => $rcObj->getTitle(),
+               ] );
 
                // everything else: makes it easier for extensions to add or remove data
                $line .= implode( '', $data );
index 4d6187b..88c3c22 100644 (file)
@@ -142,6 +142,15 @@ class OldChangesList extends ChangesList {
                        $html .= ' ' . $this->numberofWatchingusers( $rc->numberofWatchingusers );
                }
 
+               $html = Html::rawElement( 'span', [
+                       'class' => 'mw-changeslist-line-inner',
+                       'data-target-page' => $rc->getTitle(), // Used for reliable determination of the affiliated page
+               ], $html );
+               if ( is_callable( $this->changeLinePrefixer ) ) {
+                       $prefix = call_user_func( $this->changeLinePrefixer, $rc, $this, false );
+                       $html = Html::rawElement( 'span', [ 'class' => 'mw-changeslist-line-prefix' ], $prefix ) . $html;
+               }
+
                return $html;
        }
 }
index 51ddc0b..1d76e36 100644 (file)
@@ -58,6 +58,7 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                        'mediawiki.special.changeslist.visitedstatus',
                        'mediawiki.special.watchlist',
                ] );
+               $output->addModuleStyles( [ 'mediawiki.special.watchlist.styles' ] );
 
                $mode = SpecialEditWatchlist::getMode( $request, $subpage );
                if ( $mode !== false ) {
@@ -431,6 +432,23 @@ class SpecialWatchlist extends ChangesListSpecialPage {
                $list = ChangesList::newFromContext( $this->getContext(), $this->filterGroups );
                $list->setWatchlistDivs();
                $list->initChangesListRows( $rows );
+               if ( $user->getOption( 'watchlistunwatchlinks' ) ) {
+                       $list->setChangeLinePrefixer( function ( RecentChange $rc, ChangesList $cl, $grouped ) {
+                               // Don't show unwatch link if the line is a grouped log entry using EnhancedChangesList,
+                               // since EnhancedChangesList groups log entries by performer rather than by target article
+                               if ( $rc->mAttribs['rc_type'] == RC_LOG && $cl instanceof EnhancedChangesList &&
+                                       $grouped ) {
+                                       return '';
+                               } else {
+                                       return $this->getLinkRenderer()
+                                                       ->makeKnownLink( $rc->getTitle(),
+                                                               $this->msg( 'watchlist-unwatch' )->text(), [
+                                                                       'class' => 'mw-unwatch-link',
+                                                                       'title' => $this->msg( 'tooltip-ca-unwatch' )->text()
+                                                               ], [ 'action' => 'unwatch' ] ) . '&#160;';
+                               }
+                       } );
+               }
                $dbr->dataSeek( $rows, 0 );
 
                if ( $this->getConfig()->get( 'RCShowWatchingUsers' )
index 3a37c2e..cd59d6d 100644 (file)
@@ -3,8 +3,9 @@
                <td>
                        <span class="mw-collapsible-toggle mw-collapsible-arrow mw-enhancedchanges-arrow mw-enhancedchanges-arrow-space"></span>
                </td>
+               <td class="mw-changeslist-line-prefix">{{{ prefix }}}</td>
                <td class="mw-enhanced-rc">{{{ collectedRcFlags }}}&#160;{{ timestamp }}&#160;</td>
-               <td>
+               <td class="mw-changeslist-line-inner">
                        {{# rev-deleted-event }}<span class="history-deleted">{{{ . }}}</span>{{/ rev-deleted-event }}
                        {{{ articleLink }}}{{{ languageDirMark }}}{{{ logText }}}
                        <span class="mw-changeslist-separator">. .</span>
        </tr>
        {{# lines }}
        <tr class="{{# classes }}{{ . }} {{/ classes }}"{{{ attribs }}}>
+               <td></td>
                <td></td>
                <td class="mw-enhanced-rc">{{{ recentChangesFlags }}}&#160;</td>
-               <td class="mw-enhanced-rc-nested">
+               <td class="mw-enhanced-rc-nested" data-target-page="{{ targetTitle }}">
                        {{# timestampLink }}
                        <span class="mw-enhanced-rc-time">{{{ . }}}</span>
                        {{/ timestampLink }}
index 86ac78e..d7a3aeb 100644 (file)
@@ -37,6 +37,7 @@
        "tog-watchlisthideminor": "Hide minor edits from the watchlist",
        "tog-watchlisthideliu": "Hide edits by logged in users from the watchlist",
        "tog-watchlistreloadautomatically": "Reload the watchlist automatically whenever a filter is changed (JavaScript required)",
+       "tog-watchlistunwatchlinks": "Add direct unwatch/watch links to watchlist entries (JavaScript required for toggle functionality)",
        "tog-watchlisthideanons": "Hide edits by anonymous users from the watchlist",
        "tog-watchlisthidepatrolled": "Hide patrolled edits from the watchlist",
        "tog-watchlisthidecategorization": "Hide categorization of pages",
        "watching": "Watching...",
        "unwatching": "Unwatching...",
        "watcherrortext": "An error occurred while changing your watchlist settings for \"$1\".",
+       "watchlist-unwatch": "×",
+       "watchlist-unwatch-undo": "+",
        "enotif_reset": "Mark all pages visited",
        "enotif_impersonal_salutation": "{{SITENAME}} user",
        "enotif_subject_deleted": "{{SITENAME}} page $1 has been {{GENDER:$2|deleted}} by $2",
index 01cb8ca..f7b176c 100644 (file)
        "tog-watchlisthidebots": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide bot edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthideminor": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide minor edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthideliu": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
-       "tog-watchlistreloadautomatically": "[[Special:Preferences]], tab 'Watchlist'. Offers user to to automatically refresh the watchlist page, when a filter is changed.",
+       "tog-watchlistreloadautomatically": "[[Special:Preferences]], tab 'Watchlist'. Offers user to automatically refresh the watchlist page, when a filter is changed.",
+       "tog-watchlistunwatchlinks": "[[Special:Preferences]], tab 'Watchlist'. Offers user to add an unwatch/watch toggle link to watchlist entries.",
        "tog-watchlisthideanons": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthidepatrolled": "Option in Watchlist tab of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}",
        "tog-watchlisthidecategorization": "Option in Watchlist tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages. Appears next to checkboxes with labels such as {{msg-mw|tog-watchlisthideminor}}.",
        "watching": "Text displayed when clicked on the watch tab: {{msg-mw|Watch}}. It means the wiki is adding that page to your watchlist.",
        "unwatching": "Text displayed when clicked on the unwatch tab: {{msg-mw|Unwatch}}. It means the wiki is removing that page from your watchlist.",
        "watcherrortext": "When a user clicked the watch/unwatch tab and the action did not succeed, this message is displayed.\n\nThis message is used raw and should not contain wikitext.\n\nParameters:\n* $1 - ...\nSee also:\n* {{msg-mw|Addedwatchtext}}",
+       "watchlist-unwatch": "Symbol used for the link to unwatch a page from the watchlist.",
+       "watchlist-unwatch-undo": "Symbol used for the link to re-watch a page that has been unwatched from the watchlist.",
        "enotif_reset": "Used in [[Special:Watchlist]].\n\nThis should be translated as \"Mark all pages '''as''' visited\".\n\nSee also:\n* {{msg-mw|Watchlist-options|fieldset}}\n* {{msg-mw|Watchlist-details|watchlist header}}\n* {{msg-mw|Wlheader-enotif|watchlist header}}",
        "enotif_impersonal_salutation": "Used for impersonal e-mail notifications, suitable for bulk mailing.\n{{Identical|User}}",
        "enotif_subject_deleted": "Email notification subject for deleted pages. Parameters:\n* $1 - page title\n* $2 - username who has deleted the page, can be used for GENDER",
index f004d62..144747b 100644 (file)
@@ -2218,11 +2218,27 @@ return [
        ],
        'mediawiki.special.watchlist' => [
                'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js',
+               'messages' => [
+                       'addedwatchtext',
+                       'addedwatchtext-talk',
+                       'removedwatchtext',
+                       'removedwatchtext-talk',
+                       'tooltip-ca-watch',
+                       'tooltip-ca-unwatch',
+                       'watchlist-unwatch',
+                       'watchlist-unwatch-undo',
+               ],
                'dependencies' => [
-                       'mediawiki.api',
+                       'mediawiki.api.watch',
+                       'mediawiki.jqueryMsg',
+                       'mediawiki.Title',
+                       'mediawiki.util',
                        'oojs-ui-core',
                        'user.options',
-               ]
+               ],
+       ],
+       'mediawiki.special.watchlist.styles' => [
+               'styles' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.css',
        ],
        'mediawiki.special.version' => [
                'styles' => 'resources/src/mediawiki.special/mediawiki.special.version.css',
diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.css b/resources/src/mediawiki.special/mediawiki.special.watchlist.css
new file mode 100644 (file)
index 0000000..c9861c2
--- /dev/null
@@ -0,0 +1,15 @@
+/*!
+ * Styling for elements generated by JavaScript on Special:Watchlist
+ */
+.mw-changelist-line-inner-unwatched {
+       text-decoration: line-through;
+       opacity: 0.5;
+}
+
+span.mw-changeslist-line-prefix {
+       display: inline-block;
+}
+/* This can be either a span or a table cell */
+.mw-changeslist-line-prefix {
+       width: 1.25em;
+}
index 7cc9b9b..535ca93 100644 (file)
@@ -3,7 +3,7 @@
  */
 ( function ( mw, $, OO ) {
        $( function () {
-               var $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
+               var api = new mw.Api(), $progressBar, $resetForm = $( '#mw-watchlist-resetbutton' );
 
                // If the user wants to reset their watchlist, use an API call to do so (no reload required)
                // Adapted from a user script by User:NQ of English Wikipedia
@@ -19,8 +19,7 @@
                        if ( !$progressBar ) {
                                $progressBar = new OO.ui.ProgressBarWidget( { progress: false } ).$element;
                                $progressBar.css( {
-                                       position: 'absolute',
-                                       width: '100%'
+                                       position: 'absolute', width: '100%'
                                } );
                        }
                        // Show progress bar
 
                        // Use action=setnotificationtimestamp to mark all as visited,
                        // then set all watchlist lines accordingly
-                       new mw.Api().postWithToken( 'csrf', {
-                               formatversion: 2,
-                               action: 'setnotificationtimestamp',
-                               entirewatchlist: true
+                       api.postWithToken( 'csrf', {
+                               formatversion: 2, action: 'setnotificationtimestamp', entirewatchlist: true
                        } ).done( function () {
                                // Enable button again
                                $button.prop( 'disabled', false );
                                $( '#mw-watchlist-form' ).submit();
                        } );
                }
+
+               if ( mw.user.options.get( 'watchlistunwatchlinks' ) ) {
+                       // Watch/unwatch toggle link:
+                       // If a page is on the watchlist, a '×' is shown which, when clicked, removes the page from the watchlist.
+                       // After unwatching a page, the '×' becomes a '+', which if clicked re-watches the page.
+                       // Unwatched page entries are struck through and have lowered opacity.
+                       $( '.mw-unwatch-link, .mw-watch-link' ).click( function ( event ) {
+                               var $unwatchLink = $( this ), // EnhancedChangesList uses <table> for each row, while OldChangesList uses <li> for each row
+                                       $watchlistLine = $unwatchLink.closest( 'li, table' )
+                                               .find( '[data-target-page]' ),
+                                       pageTitle = $watchlistLine.data( 'targetPage' ),
+                                       isTalk = mw.Title.newFromText( pageTitle ).getNamespaceId() % 2 === 1;
+
+                               // Utility function for looping through each watchlist line that matches
+                               // a certain page or its associated page (e.g. Talk)
+                               function forEachMatchingTitle( title, callback ) {
+
+                                       var titleObj = mw.Title.newFromText( title ),
+                                               pageNamespaceId = titleObj.getNamespaceId(),
+                                               isTalk = pageNamespaceId % 2 === 1,
+                                               associatedTitle = mw.Title.makeTitle( isTalk ? pageNamespaceId - 1 : pageNamespaceId + 1,
+                                                       titleObj.getMainText() ).getPrefixedText();
+                                       $( '.mw-changeslist-line' ).each( function () {
+                                               var $this = $( this ), $row, $unwatchLink;
+
+                                               $this.find( '[data-target-page]' ).each( function () {
+                                                       var $this = $( this ), rowTitle = $this.data( 'targetPage' );
+                                                       if ( rowTitle === title || rowTitle === associatedTitle ) {
+
+                                                               // EnhancedChangesList groups log entries by performer rather than target page. Therefore...
+                                                               // * If using OldChangesList, use the <li>
+                                                               // * If using EnhancedChangesList and $this is part of a grouped log entry, use the <td> sub-entry
+                                                               // * If using EnhancedChangesList and $this is not part of a grouped log entry, use the <table> grouped entry
+                                                               $row =
+                                                                       $this.closest(
+                                                                               'li, table.mw-collapsible.mw-changeslist-log td[data-target-page], table' );
+                                                               $unwatchLink = $row.find( '.mw-unwatch-link, .mw-watch-link' );
+
+                                                               callback( rowTitle, $row, $unwatchLink );
+                                                       }
+                                               } );
+                                       } );
+                               }
+
+                               // Depending on whether we are watching or unwatching, for each entry of the page (and its associated page i.e. Talk),
+                               // change the text, tooltip, and non-JS href of the (un)watch button, and update the styling of the watchlist entry.
+                               if ( $unwatchLink.hasClass( 'mw-unwatch-link' ) ) {
+                                       api.unwatch( pageTitle )
+                                               .done( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch-undo' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-watch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'watch' } ) )
+                                                                               .removeClass( 'mw-unwatch-link loading' )
+                                                                               .addClass( 'mw-watch-link' );
+                                                                       $row.find(
+                                                                               '.mw-changeslist-line-inner, .mw-enhanced-rc-nested' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' ) // For matching log sub-entry
+                                                                               .addClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'removedwatchtext-talk' : 'removedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               } else {
+                                       api.watch( pageTitle )
+                                               .then( function () {
+                                                       forEachMatchingTitle( pageTitle,
+                                                               function ( rowPageTitle, $row, $rowUnwatchLink ) {
+                                                                       $rowUnwatchLink
+                                                                               .text( mw.msg( 'watchlist-unwatch' ) )
+                                                                               .attr( 'title', mw.msg( 'tooltip-ca-unwatch' ) )
+                                                                               .attr( 'href',
+                                                                                       mw.util.getUrl( rowPageTitle, { action: 'unwatch' } ) )
+                                                                               .removeClass( 'mw-watch-link loading' )
+                                                                               .addClass( 'mw-unwatch-link' );
+                                                                       $row.find( '.mw-changelist-line-inner-unwatched' )
+                                                                               .addBack( '.mw-enhanced-rc-nested' )
+                                                                               .removeClass( 'mw-changelist-line-inner-unwatched' );
+                                                               } );
+
+                                                       mw.notify(
+                                                               mw.message( isTalk ? 'addedwatchtext-talk' : 'addedwatchtext',
+                                                                       pageTitle ), { tag: 'watch-self' } );
+                                               } );
+                               }
+
+                               event.preventDefault();
+                               event.stopPropagation();
+                               $unwatchLink.blur();
+                       } );
+               }
        } );
 
-}( mediaWiki, jQuery, OO ) );
+}( mediaWiki, jQuery, OO )
+);
index 1290c64..420fe74 100644 (file)
@@ -74,6 +74,47 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
                $this->assertEquals( '', $html );
        }
 
+       public function testRecentChangesPrefix() {
+               $mockContext = $this->getMockBuilder( RequestContext::class )
+                       ->setMethods( [ 'getTitle' ] )
+                       ->getMock();
+               $mockContext->method( 'getTitle' )
+                       ->will( $this->returnValue( Title::newFromText( 'Expected Context Title' ) ) );
+
+               // One group of two lines
+               $enhancedChangesList = $this->newEnhancedChangesList();
+               $enhancedChangesList->setContext( $mockContext );
+               $enhancedChangesList->setChangeLinePrefixer( function ( $rc, $changesList ) {
+                       // Make sure RecentChange and ChangesList objects are the same
+                       $this->assertEquals( 'Expected Context Title', $changesList->getContext()->getTitle() );
+                       $this->assertTrue( $rc->getTitle() == 'Cat' || $rc->getTitle() == 'Dog' );
+                       return 'Hello world prefix';
+               } );
+               $enhancedChangesList->beginRecentChangesList();
+
+               $recentChange = $this->getEditChange( '20131103092153' );
+               $enhancedChangesList->recentChangesLine( $recentChange );
+               $recentChange = $this->getEditChange( '20131103092154' );
+               $enhancedChangesList->recentChangesLine( $recentChange );
+
+               $html = $enhancedChangesList->endRecentChangesList();
+
+               $this->assertRegExp( '/Hello world prefix/', $html );
+
+               // Two separate lines
+               $enhancedChangesList->beginRecentChangesList();
+
+               $recentChange = $this->getEditChange( '20131103092153' );
+               $enhancedChangesList->recentChangesLine( $recentChange );
+               $recentChange = $this->getEditChange( '20131103092154', 'Dog' );
+               $enhancedChangesList->recentChangesLine( $recentChange );
+
+               $html = $enhancedChangesList->endRecentChangesList();
+
+               preg_match_all( '/Hello world prefix/', $html, $matches );
+               $this->assertCount( 2, $matches[0] );
+       }
+
        public function testCategorizationLineFormatting() {
                $html = $this->createCategorizationLine(
                        $this->getCategorizationChange( '20150629191735', 0, 0 )
@@ -112,12 +153,16 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
                preg_match_all( '/td class="mw-enhanced-rc-nested"/', $html, $matches );
                $this->assertCount( 2, $matches[0] );
 
+               preg_match_all( '/data-target-page="Cat"/', $html, $matches );
+               $this->assertCount( 2, $matches[0] );
+
                $recentChange3 = $this->getLogChange();
                $enhancedChangesList->recentChangesLine( $recentChange3, false );
 
                $html = $enhancedChangesList->endRecentChangesList();
                $this->assertContains( 'data-mw-logaction="foo/bar"', $html );
                $this->assertContains( 'data-mw-logid="25"', $html );
+               $this->assertContains( 'data-target-page="Title"', $html );
        }
 
        /**
@@ -133,10 +178,10 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
        /**
         * @return RecentChange
         */
-       private function getEditChange( $timestamp ) {
+       private function getEditChange( $timestamp, $pageTitle = 'Cat' ) {
                $user = $this->getMutableTestUser()->getUser();
                $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
-                       $user, 'Cat', 0, 5, 191, $timestamp, 0, 0
+                       $user, $pageTitle, 0, 5, 191, $timestamp, 0, 0
                );
 
                return $recentChange;
index 244a05d..91dc731 100644 (file)
@@ -155,6 +155,40 @@ class OldChangesListTest extends MediaWikiLangTestCase {
                $this->assertRegExp( "/watchlist-0-Cat/", $line );
        }
 
+       public function testRecentChangesLine_dataAttribute() {
+               $oldChangesList = $this->getOldChangesList();
+               $oldChangesList->setWatchlistDivs( true );
+
+               $recentChange = $this->getEditChange();
+               $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+               $this->assertRegExp( '/data-target-page=\"Cat\"/', $line );
+
+               $recentChange = $this->getLogChange( 'delete', 'delete' );
+               $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
+               $this->assertRegExp( '/data-target-page="Abc"/', $line );
+       }
+
+       public function testRecentChangesLine_prefix() {
+               $mockContext = $this->getMockBuilder( RequestContext::class )
+                       ->setMethods( [ 'getTitle' ] )
+                       ->getMock();
+               $mockContext->method( 'getTitle' )
+                       ->will( $this->returnValue( Title::newFromText( 'Expected Context Title' ) ) );
+
+               $oldChangesList = $this->getOldChangesList();
+               $oldChangesList->setContext( $mockContext );
+               $recentChange = $this->getEditChange();
+
+               $oldChangesList->setChangeLinePrefixer( function ( $rc, $changesList ) {
+                       // Make sure RecentChange and ChangesList objects are the same
+                       $this->assertEquals( 'Expected Context Title', $changesList->getContext()->getTitle() );
+                       $this->assertEquals( 'Cat', $rc->getTitle() );
+                       return 'I am a prefix';
+               } );
+               $line = $oldChangesList->recentChangesLine( $recentChange );
+               $this->assertRegExp( "/I am a prefix/", $line );
+       }
+
        private function getNewBotEditChange() {
                $user = $this->getMutableTestUser()->getUser();
 
index b0490ec..1c43919 100644 (file)
@@ -41,6 +41,7 @@ class SpecialWatchlistTest extends SpecialPageTestBase {
                                'watchlisthidepatrolled' => 0,
                                'watchlisthidecategorization' => 1,
                                'watchlistreloadautomatically' => 0,
+                               'watchlistunwatchlinks' => 0,
                        ]
                );
        }