RCFilters: show new changes
authorStephane Bisson <sbisson@wikimedia.org>
Fri, 21 Jul 2017 15:41:36 +0000 (11:41 -0400)
committerStephane Bisson <sbisson@wikimedia.org>
Mon, 31 Jul 2017 12:50:09 +0000 (08:50 -0400)
When "live update" is off and new changes are detected,
show a link to load and prepend the changes to the list.

Also adding a line between old and new changes
when grouping by pages is off.

Bug: T163426
Change-Id: I6a111d23956bdc04caa4c71e9deede056779aafa

16 files changed:
includes/changes/ChangesList.php
includes/specials/SpecialRecentchanges.php
languages/i18n/en.json
languages/i18n/qqq.json
resources/Resources.php
resources/src/mediawiki.rcfilters/dm/mw.rcfilters.dm.ChangesListViewModel.js
resources/src/mediawiki.rcfilters/mw.rcfilters.Controller.js
resources/src/mediawiki.rcfilters/mw.rcfilters.init.js
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less
resources/src/mediawiki.rcfilters/styles/mw.rcfilters.ui.ChangesListWrapperWidget.less
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.ChangesListWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FilterWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.FormWrapperWidget.js
resources/src/mediawiki.rcfilters/ui/mw.rcfilters.ui.LiveUpdateButtonWidget.js
tests/phpunit/includes/changes/EnhancedChangesListTest.php
tests/phpunit/includes/changes/OldChangesListTest.php

index 5aa693d..2182c6c 100644 (file)
@@ -747,20 +747,22 @@ class ChangesList extends ContextSource {
         * @return string[] attribute name => value
         */
        protected function getDataAttributes( RecentChange $rc ) {
+               $attrs = [];
+
                $type = $rc->getAttribute( 'rc_source' );
                switch ( $type ) {
                        case RecentChange::SRC_EDIT:
                        case RecentChange::SRC_NEW:
-                               return [
-                                       'data-mw-revid' => $rc->mAttribs['rc_this_oldid'],
-                               ];
+                               $attrs[ 'data-mw-revid' ] = $rc->mAttribs['rc_this_oldid'];
+                               break;
                        case RecentChange::SRC_LOG:
-                               return [
-                                       'data-mw-logid' => $rc->mAttribs['rc_logid'],
-                                       'data-mw-logaction' => $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'],
-                               ];
-                       default:
-                               return [];
+                               $attrs[ 'data-mw-logid' ] = $rc->mAttribs['rc_logid'];
+                               $attrs[ 'data-mw-logaction' ] = $rc->mAttribs['rc_log_type'] . '/' . $rc->mAttribs['rc_log_action'];
+                               break;
                }
+
+               $attrs[ 'data-mw-ts' ] = $rc->getAttribute( 'rc_timestamp' );
+
+               return $attrs;
        }
 }
index f9052ad..8d00e90 100644 (file)
@@ -973,15 +973,20 @@ class SpecialRecentChanges extends ChangesListSpecialPage {
                        $resetLink = $this->makeOptionsLink( $this->msg( 'rclistfromreset' ),
                                [ 'from' => '' ], $nondefaults );
 
-                       $note .= $this->msg( 'rcnotefrom' )
+                       $noteFromMsg = $this->msg( 'rcnotefrom' )
                                ->numParams( $options['limit'] )
                                ->params(
                                        $lang->userTimeAndDate( $options['from'], $user ),
                                        $lang->userDate( $options['from'], $user ),
                                        $lang->userTime( $options['from'], $user )
                                )
-                               ->numParams( $numRows )
-                               ->parse() . ' ' .
+                               ->numParams( $numRows );
+                       $note .= Html::rawElement(
+                                       'span',
+                                       [ 'class' => 'rcnotefrom' ],
+                                       $noteFromMsg->parse()
+                               ) .
+                               ' ' .
                                Html::rawElement(
                                        'span',
                                        [ 'class' => 'rcoptions-listfromreset' ],
index b2b4179..8c295b2 100644 (file)
        "rcfilters-savedqueries-add-new-title": "Save current filter settings",
        "rcfilters-restore-default-filters": "Restore default filters",
        "rcfilters-clear-all-filters": "Clear all filters",
+       "rcfilters-show-new-changes": "Show new changes",
+       "rcfilters-previous-changes-label": "Previously viewed changes",
        "rcfilters-search-placeholder": "Filter recent changes (browse or start typing)",
        "rcfilters-invalid-filter": "Invalid filter",
        "rcfilters-empty-filter": "No active filters. All contributions are shown.",
index f2755d1..89f6f41 100644 (file)
        "rcfilters-savedqueries-add-new-title": "Title for the popup to add new quick link in [[Special:RecentChanges]]. This is for a small popup, please try to use a short string.",
        "rcfilters-restore-default-filters": "Label for the button that resets filters to defaults",
        "rcfilters-clear-all-filters": "Title for the button that clears all filters",
+       "rcfilters-show-new-changes": "Label for the button to show new changes.",
+       "rcfilters-previous-changes-label": "Label to indicate the changes below have been previously viewed.",
        "rcfilters-search-placeholder": "Placeholder for the filter search input.",
        "rcfilters-invalid-filter": "A label for an invalid filter.",
        "rcfilters-empty-filter": "Placeholder for the filter list when no filters were chosen.",
index e3d8a1b..82f285e 100644 (file)
@@ -1857,6 +1857,8 @@ return [
                        'rcfilters-savedqueries-cancel-label',
                        'rcfilters-restore-default-filters',
                        'rcfilters-clear-all-filters',
+                       'rcfilters-show-new-changes',
+                       'rcfilters-previous-changes-label',
                        'rcfilters-search-placeholder',
                        'rcfilters-invalid-filter',
                        'rcfilters-empty-filter',
index 49c0b82..34ed2eb 100644 (file)
@@ -11,6 +11,9 @@
                OO.EventEmitter.call( this );
 
                this.valid = true;
+               this.newChangesExist = false;
+               this.nextFrom = null;
+               this.liveUpdate = false;
        };
 
        /* Initialization */
 
        /**
         * @event update
-        * @param {jQuery|string} changesListContent
-        * @param {jQuery} $fieldset
+        * @param {jQuery|string} $changesListContent List of changes
+        * @param {jQuery} $fieldset Server-generated form
+        * @param {boolean} isInitialDOM Whether the previous dom variables are from the initial page load
+        * @param {boolean} fromLiveUpdate These are new changes fetched via Live Update
+        *
+        * The list of changes has been updated
+        */
+
+       /**
+        * @event newChangesExist
+        * @param {boolean} newChangesExist
+        *
+        * The existence of changes newer than those currently displayed has changed.
+        */
+
+       /**
+        * @event liveUpdateChange
+        * @param {boolean} enable
         *
-        * The list of change is now up to date
+        * The state of the 'live update' feature has changed.
         */
 
        /* Methods */
         * @param {jQuery|string} changesListContent
         * @param {jQuery} $fieldset
         * @param {boolean} [isInitialDOM] Using the initial (already attached) DOM elements
+        * @param {boolean} [fromLiveUpdate] These are new changes fetched via Live Update
+        * @fires update
         */
-       mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, isInitialDOM ) {
+       mw.rcfilters.dm.ChangesListViewModel.prototype.update = function ( changesListContent, $fieldset, isInitialDOM, fromLiveUpdate ) {
+               var from = this.nextFrom;
                this.valid = true;
-               this.emit( 'update', changesListContent, $fieldset, isInitialDOM );
+               this.extractNextFrom( $fieldset );
+               this.emit( 'update', changesListContent, $fieldset, isInitialDOM, fromLiveUpdate ? from : null );
+       };
+
+       /**
+        * Specify whether new changes exist
+        *
+        * @param {boolean} newChangesExist
+        * @fires newChangesExist
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.setNewChangesExist = function ( newChangesExist ) {
+               if ( newChangesExist !== this.newChangesExist ) {
+                       this.newChangesExist = newChangesExist;
+                       this.emit( 'newChangesExist', newChangesExist );
+               }
+       };
+
+       /**
+        * @return {boolean} Whether new changes exist
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.getNewChangesExist = function () {
+               return this.newChangesExist;
+       };
+
+       /**
+        * Extract the value of the 'from' parameter from a link in the field set
+        *
+        * @param {jQuery} $fieldset
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.extractNextFrom = function ( $fieldset ) {
+               this.nextFrom = $fieldset.find( '.rclistfrom > a' ).data( 'params' ).from;
+       };
+
+       /**
+        * @return {string} The 'from' parameter that can be used to query new changes
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.getNextFrom = function () {
+               return this.nextFrom;
+       };
+
+       /**
+        * Toggle the 'live update' feature on/off
+        *
+        * @param {boolean} enable
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.toggleLiveUpdate = function ( enable ) {
+               enable = enable === undefined ? !this.liveUpdate : enable;
+               if ( enable !== this.liveUpdate ) {
+                       this.liveUpdate = enable;
+                       this.emit( 'liveUpdateChange', this.liveUpdate );
+               }
+       };
+
+       /**
+        * @return {boolean} The 'live update' feature is enabled
+        */
+       mw.rcfilters.dm.ChangesListViewModel.prototype.getLiveUpdate = function () {
+               return this.liveUpdate;
        };
 
 }( mediaWiki ) );
index 73ff165..a65a9c2 100644 (file)
@@ -13,7 +13,7 @@
                this.filtersModel = filtersModel;
                this.changesListModel = changesListModel;
                this.savedQueriesModel = savedQueriesModel;
-               this.requestCounter = 0;
+               this.requestCounter = {};
                this.baseFilterState = {};
                this.uriProcessor = null;
                this.initializing = false;
 
                this.initializing = false;
                this.switchView( 'default' );
+
+               this._scheduleLiveUpdate();
        };
 
        /**
         * @param {boolean} enable True to enable, false to disable
         */
        mw.rcfilters.Controller.prototype.toggleLiveUpdate = function ( enable ) {
-               if ( enable && !this.liveUpdateTimeout ) {
-                       this._scheduleLiveUpdate();
-               } else if ( !enable && this.liveUpdateTimeout ) {
-                       clearTimeout( this.liveUpdateTimeout );
-                       this.liveUpdateTimeout = null;
+               this.changesListModel.toggleLiveUpdate( enable );
+               if ( this.changesListModel.getLiveUpdate() && this.changesListModel.getNewChangesExist() ) {
+                       this.showNewChanges();
                }
        };
 
         * @private
         */
        mw.rcfilters.Controller.prototype._scheduleLiveUpdate = function () {
-               this.liveUpdateTimeout = setTimeout( this._doLiveUpdate.bind( this ), 3000 );
+               setTimeout( this._doLiveUpdate.bind( this ), 3000 );
        };
 
        /**
         * @private
         */
        mw.rcfilters.Controller.prototype._doLiveUpdate = function () {
-               var controller = this;
-               this.updateChangesList( {}, true )
-                       .always( function () {
-                               if ( controller.liveUpdateTimeout ) {
-                                       // Live update was not disabled in the meantime
-                                       controller._scheduleLiveUpdate();
+               if ( !this._shouldCheckForNewChanges() ) {
+                       // skip this turn and check back later
+                       this._scheduleLiveUpdate();
+                       return;
+               }
+
+               this._checkForNewChanges()
+                       .then( function ( data ) {
+                               if ( !this._shouldCheckForNewChanges() ) {
+                                       // by the time the response is received,
+                                       // it may not be appropriate anymore
+                                       return;
                                }
-                       } );
+
+                               if ( data.changes !== 'NO_RESULTS' ) {
+                                       if ( this.changesListModel.getLiveUpdate() ) {
+                                               return this.updateChangesList( false, null, true, false );
+                                       } else {
+                                               this.changesListModel.setNewChangesExist( true );
+                                       }
+                               }
+                       }.bind( this ) )
+                       .always( this._scheduleLiveUpdate.bind( this ) );
+       };
+
+       /**
+        * @return {boolean} It's appropriate to check for new changes now
+        * @private
+        */
+       mw.rcfilters.Controller.prototype._shouldCheckForNewChanges = function () {
+               var liveUpdateFeatureFlag = mw.config.get( 'wgStructuredChangeFiltersEnableLiveUpdate' ) ||
+                       new mw.Uri().query.liveupdate;
+
+               return !document.hidden &&
+                       !this.changesListModel.getNewChangesExist() &&
+                       !this.updatingChangesList &&
+                       liveUpdateFeatureFlag;
+       };
+
+       /**
+        * Check if new changes, newer than those currently shown, are available
+        *
+        * @return {jQuery.Promise} Promise object that resolves after trying
+        * to fetch 1 change newer than the last known 'from' parameter value
+        *
+        * @private
+        */
+       mw.rcfilters.Controller.prototype._checkForNewChanges = function () {
+               return this._fetchChangesList(
+                       'liveUpdate',
+                       {
+                               limit: 1,
+                               from: this.changesListModel.getNextFrom()
+                       }
+               );
+       };
+
+       /**
+        * Show the new changes
+        *
+        * @return {jQuery.Promise} Promise object that resolves after
+        * fetching and showing the new changes
+        */
+       mw.rcfilters.Controller.prototype.showNewChanges = function () {
+               return this.updateChangesList( false, null, true, true );
        };
 
        /**
        /**
         * Update the list of changes and notify the model
         *
+        * @param {boolean} [updateUrl=true] Whether the URL should be updated with the current state of the filters
         * @param {Object} [params] Extra parameters to add to the API call
-        * @param {boolean} [isLiveUpdate] Don't update the URL or invalidate the changes list
+        * @param {boolean} [isLiveUpdate=false] The purpose of this update is to show new results for the same filters
+        * @param {boolean} [invalidateCurrentChanges=true] Invalidate current changes by default (show spinner)
         * @return {jQuery.Promise} Promise that is resolved when the update is complete
         */
-       mw.rcfilters.Controller.prototype.updateChangesList = function ( params, isLiveUpdate ) {
-               if ( !isLiveUpdate ) {
+       mw.rcfilters.Controller.prototype.updateChangesList = function ( updateUrl, params, isLiveUpdate, invalidateCurrentChanges ) {
+               updateUrl = updateUrl === undefined ? true : updateUrl;
+               invalidateCurrentChanges = invalidateCurrentChanges === undefined ? true : invalidateCurrentChanges;
+               if ( updateUrl ) {
                        this._updateURL( params );
+               }
+               if ( invalidateCurrentChanges ) {
                        this.changesListModel.invalidate();
                }
+               this.changesListModel.setNewChangesExist( false );
+               this.updatingChangesList = true;
                return this._fetchChangesList()
                        .then(
                                // Success
                                function ( pieces ) {
                                        var $changesListContent = pieces.changes,
                                                $fieldset = pieces.fieldset;
-                                       this.changesListModel.update( $changesListContent, $fieldset );
+                                       this.changesListModel.update( $changesListContent, $fieldset, false, isLiveUpdate );
                                }.bind( this )
                                // Do nothing for failure
-                       );
+                       )
+                       .always( function () {
+                               this.updatingChangesList = false;
+                       }.bind( this ) );
        };
 
        /**
        /**
         * Fetch the list of changes from the server for the current filters
         *
+        * @param {string} [counterId='updateChangesList'] Id for this request. To allow concurrent requests
+        *  not to invalidate each other.
+        * @param {Object} [params={}] Parameters to add to the query
+        *
         * @return {jQuery.Promise} Promise object that will resolve with the changes list
         *  or with a string denoting no results.
         */
-       mw.rcfilters.Controller.prototype._fetchChangesList = function () {
+       mw.rcfilters.Controller.prototype._fetchChangesList = function ( counterId, params ) {
                var uri = this._getUpdatedUri(),
                        stickyParams = this.filtersModel.getStickyParams(),
-                       requestId = ++this.requestCounter,
-                       latestRequest = function () {
-                               return requestId === this.requestCounter;
-                       }.bind( this );
+                       requestId,
+                       latestRequest;
+
+               counterId = counterId || 'updateChangesList';
+               params = params || {};
+
+               uri.extend( params );
+
+               this.requestCounter[ counterId ] = this.requestCounter[ counterId ] || 0;
+               requestId = ++this.requestCounter[ counterId ];
+               latestRequest = function () {
+                       return requestId === this.requestCounter[ counterId ];
+               }.bind( this );
 
                // Sticky parameters override the URL params
                // this is to make sure that whether we represent
index 12a83cd..701e61d 100644 (file)
                                $overlay = $( '<div>' )
                                        .addClass( 'mw-rcfilters-ui-overlay' ),
                                filtersWidget = new mw.rcfilters.ui.FilterWrapperWidget(
-                                       controller, filtersModel, savedQueriesModel, { $overlay: $overlay } );
+                                       controller, filtersModel, savedQueriesModel, changesListModel, { $overlay: $overlay } );
 
                        // TODO: The changesListWrapperWidget should be able to initialize
                        // after the model is ready.
                        // eslint-disable-next-line no-new
                        new mw.rcfilters.ui.ChangesListWrapperWidget(
-                               filtersModel, changesListModel, $( '.mw-changeslist, .mw-changeslist-empty' ) );
+                               filtersModel, changesListModel, controller, $( '.mw-changeslist, .mw-changeslist-empty' ) );
 
                        controller.initialize(
                                mw.config.get( 'wgStructuredChangeFilters' ),
index 0d12b81..7f0d34e 100644 (file)
@@ -2,11 +2,6 @@
 .client-js {
        .rcoptions {
                border: 0;
-               border-bottom: 1px solid #a2a9b1;
-
-               legend {
-                       display: none;
-               }
        }
 
        .mw-recentchanges-toplinks {
@@ -29,7 +24,7 @@
        }
 
        .rcfilters-head {
-               min-height: 310px;
+               min-height: 220px;
                margin-top: 1em;
 
                &:not( .mw-rcfilters-ui-ready ) {
index 89acdc0..d60e616 100644 (file)
@@ -1,6 +1,26 @@
 @import 'mw.rcfilters.mixins';
 
 .mw-rcfilters-ui-changesListWrapperWidget {
+
+       &-newChanges {
+               min-height: 34px;
+               margin: 10px 0;
+               text-align: center;
+       }
+
+       &-previousChangesIndicator {
+               margin: 10px 0;
+               color: #36c;
+               border-top: 2px solid #36c;
+               text-align: center;
+
+               &:hover {
+                       color: #72777d;
+                       border-top-color: #72777d;
+                       cursor: pointer;
+               }
+       }
+
        &-results {
                width: 35em;
                margin: 5em auto;
index c2533df..d571774 100644 (file)
@@ -7,12 +7,14 @@
         * @constructor
         * @param {mw.rcfilters.dm.FiltersViewModel} filtersViewModel View model
         * @param {mw.rcfilters.dm.ChangesListViewModel} changesListViewModel View model
+        * @param {mw.rcfilters.Controller} controller
         * @param {jQuery} $changesListRoot Root element of the changes list to attach to
-        * @param {Object} config Configuration object
+        * @param {Object} [config] Configuration object
         */
        mw.rcfilters.ui.ChangesListWrapperWidget = function MwRcfiltersUiChangesListWrapperWidget(
                filtersViewModel,
                changesListViewModel,
+               controller,
                $changesListRoot,
                config
        ) {
@@ -25,6 +27,7 @@
 
                this.filtersViewModel = filtersViewModel;
                this.changesListViewModel = changesListViewModel;
+               this.controller = controller;
 
                // Events
                this.filtersViewModel.connect( this, {
@@ -33,7 +36,8 @@
                } );
                this.changesListViewModel.connect( this, {
                        invalidate: 'onModelInvalidate',
-                       update: 'onModelUpdate'
+                       update: 'onModelUpdate',
+                       newChangesExist: 'onNewChangesExist'
                } );
 
                this.$element
@@ -43,6 +47,8 @@
 
                // Set up highlight containers
                this.setupHighlightContainers( this.$element );
+
+               this.setupNewChangesButtonContainer( this.$element );
        };
 
        /* Initialization */
         * @param {jQuery|string} $changesListContent The content of the updated changes list
         * @param {jQuery} $fieldset The content of the updated fieldset
         * @param {boolean} isInitialDOM Whether $changesListContent is the existing (already attached) DOM
+        * @param {boolean} from Timestamp of the new changes
         */
-       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function ( $changesListContent, $fieldset, isInitialDOM ) {
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onModelUpdate = function (
+               $changesListContent, $fieldset, isInitialDOM, from
+       ) {
                var conflictItem,
                        $message = $( '<div>' )
                                .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-results' ),
-                       isEmpty = $changesListContent === 'NO_RESULTS';
+                       isEmpty = $changesListContent === 'NO_RESULTS',
+                       $lastSeen,
+                       $indicator,
+                       $newChanges = $( [] );
 
                this.$element.toggleClass( 'mw-changeslist', !isEmpty );
                if ( isEmpty ) {
-                       this.$changesListContent = null;
                        this.$element.empty();
 
                        if ( this.filtersViewModel.hasConflict() ) {
 
                        this.$element.append( $message );
                } else {
-                       this.$changesListContent = $changesListContent;
                        if ( !isInitialDOM ) {
-                               this.$element.empty().append( this.$changesListContent );
+                               this.$element.empty().append( $changesListContent );
+
+                               if ( from ) {
+                                       $lastSeen = null;
+                                       this.$element.find( 'li[data-mw-ts]' ).each( function () {
+                                               var $li = $( this ),
+                                                       ts = $li.data( 'mw-ts' );
+
+                                               if ( ts >= from ) {
+                                                       $newChanges = $newChanges.add( $li );
+                                               } else if ( $lastSeen === null ) {
+                                                       $lastSeen = $li;
+                                                       return false;
+                                               }
+                                       } );
+
+                                       if ( $lastSeen ) {
+                                               $indicator = $( '<div>' )
+                                                       .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-previousChangesIndicator' )
+                                                       .text( mw.message( 'rcfilters-previous-changes-label' ).text() );
+
+                                               $indicator.on( 'click', function () {
+                                                       $indicator.detach();
+                                               } );
+
+                                               $lastSeen.before( $indicator );
+                                       }
+
+                                       $newChanges
+                                               .hide()
+                                               .fadeIn( 1000 );
+                               }
                        }
+
                        // Set up highlight containers
                        this.setupHighlightContainers( this.$element );
 
                this.$element.addClass( 'mw-rcfilters-ui-ready' );
        };
 
+       /**
+        * Respond to changes list model newChangesExist
+        *
+        * @param {boolean} newChangesExist Whether new changes exist
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onNewChangesExist = function ( newChangesExist ) {
+               this.showNewChangesLink.toggle( newChangesExist );
+       };
+
+       /**
+        * Respond to the user clicking the 'show new changes' button
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.onShowNewChangesClick = function () {
+               this.controller.showNewChanges();
+       };
+
+       /**
+        * Setup the container for the 'new changes' button.
+        *
+        * @param {jQuery} $content
+        */
+       mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupNewChangesButtonContainer = function ( $content ) {
+               this.showNewChangesLink = new OO.ui.ButtonWidget( {
+                       framed: false,
+                       label: mw.message( 'rcfilters-show-new-changes' ).text(),
+                       flags: [ 'progressive' ]
+               } );
+               this.showNewChangesLink.connect( this, { click: 'onShowNewChangesClick' } );
+               this.showNewChangesLink.toggle( false );
+
+               $content.before(
+                       $( '<div>' )
+                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-newChanges' )
+                               .append( this.showNewChangesLink.$element )
+               );
+       };
+
        /**
         * Set up the highlight containers with all color circle indicators.
         *
         */
        mw.rcfilters.ui.ChangesListWrapperWidget.prototype.setupHighlightContainers = function ( $content ) {
                var uri = new mw.Uri(),
+                       highlightClass = 'mw-rcfilters-ui-changesListWrapperWidget-highlights',
                        $highlights = $( '<div>' )
-                               .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights' )
+                               .addClass( highlightClass )
                                .append(
                                        $( '<div>' )
                                                .addClass( 'mw-rcfilters-ui-changesListWrapperWidget-highlights-color-none' )
index a6b363d..d3cbc20 100644 (file)
@@ -9,11 +9,14 @@
         * @param {mw.rcfilters.Controller} controller Controller
         * @param {mw.rcfilters.dm.FiltersViewModel} model View model
         * @param {mw.rcfilters.dm.SavedQueriesModel} savedQueriesModel Saved queries model
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
         * @param {Object} [config] Configuration object
         * @cfg {Object} [filters] A definition of the filter groups in this list
         * @cfg {jQuery} [$overlay] A jQuery object serving as overlay for popups
         */
-       mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget( controller, model, savedQueriesModel, config ) {
+       mw.rcfilters.ui.FilterWrapperWidget = function MwRcfiltersUiFilterWrapperWidget(
+               controller, model, savedQueriesModel, changesListModel, config
+       ) {
                var $top, $topRow, $bottom;
                config = config || {};
 
@@ -35,7 +38,8 @@
                );
 
                this.liveUpdateButton = new mw.rcfilters.ui.LiveUpdateButtonWidget(
-                       this.controller
+                       this.controller,
+                       changesListModel
                );
 
                this.numChangesWidget = new mw.rcfilters.ui.ChangesLimitButtonWidget(
index 50e3637..44f6da7 100644 (file)
@@ -54,7 +54,7 @@
         * @return {boolean} false
         */
        mw.rcfilters.ui.FormWrapperWidget.prototype.onLinkClick = function ( e ) {
-               this.controller.updateChangesList( $( e.target ).data( 'params' ) );
+               this.controller.updateChangesList( true, $( e.target ).data( 'params' ) );
                return false;
        };
 
@@ -78,7 +78,7 @@
                        data[ $( this ).prop( 'name' ) ] = value;
                } );
 
-               this.controller.updateChangesList( data );
+               this.controller.updateChangesList( true, data );
                return false;
        };
 
                        this.$element.find( '.mw-recentchanges-table' ).detach();
                        this.$element.find( 'hr' ).detach();
                }
+
                if ( !this.$element.find( '.rcshowhide' ).contents().length ) {
                        this.$element.find( '.rcshowhide' ).detach();
                        // If we're hiding rcshowhide, the '<br>'s are around it,
                        // there's no need for them either.
                        this.$element.find( 'br' ).detach();
                }
+
+               this.$element.find(
+                       'legend, .rclistfrom, .rcnotefrom, .rcoptions-listfromreset'
+               ).detach();
+
+               if ( this.$element.text().trim() === '' ) {
+                       this.$element.detach();
+               }
        };
 }( mediaWiki ) );
index 90ee4d7..67c113d 100644 (file)
@@ -6,9 +6,10 @@
         *
         * @constructor
         * @param {mw.rcfilters.Controller} controller
-        * @param {Object} config Configuration object
+        * @param {mw.rcfilters.dm.ChangesListViewModel} changesListModel
+        * @param {Object} [config] Configuration object
         */
-       mw.rcfilters.ui.LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, config ) {
+       mw.rcfilters.ui.LiveUpdateButtonWidget = function MwRcfiltersUiLiveUpdateButtonWidget( controller, changesListModel, config ) {
                config = config || {};
 
                // Parent
                }, config ) );
 
                this.controller = controller;
+               this.model = changesListModel;
 
                // Events
-               this.connect( this, { change: 'onChange' } );
+               this.connect( this, { click: 'onClick' } );
+               this.model.connect( this, { liveUpdateChange: 'onLiveUpdateChange' } );
 
                this.$element.addClass( 'mw-rcfilters-ui-liveUpdateButtonWidget' );
        };
        /* Methods */
 
        /**
-        * Respond to the button being toggled.
-        * @param {boolean} enable Whether the button is now pressed/enabled
+        * Respond to the button being clicked
         */
-       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onChange = function ( enable ) {
-               this.controller.toggleLiveUpdate( enable );
+       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onClick = function () {
+               this.controller.toggleLiveUpdate();
+       };
+
+       /**
+        * Respond to the 'live update' feature being turned on/off
+        *
+        * @param {boolean} enable Whether the 'live update' feature is now on/off
+        */
+       mw.rcfilters.ui.LiveUpdateButtonWidget.prototype.onLiveUpdateChange = function ( enable ) {
+               this.setValue( enable );
+               this.setIcon( enable ? 'stop' : 'play' );
        };
 
 }( mediaWiki ) );
index 28818d9..465bc22 100644 (file)
@@ -99,7 +99,7 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
                $enhancedChangesList->recentChangesLine( $recentChange, false );
 
                $html = $enhancedChangesList->endRecentChangesList();
-               $this->assertRegExp( '/data-mw-revid="5" class="[^"]*mw-enhanced-rc[^"]*"/', $html );
+               $this->assertRegExp( '/data-mw-revid="5" data-mw-ts="20131103092153" class="[^"]*mw-enhanced-rc[^"]*"/', $html );
 
                $recentChange2 = $this->getEditChange( '20131103092253' );
                $enhancedChangesList->recentChangesLine( $recentChange2, false );
@@ -133,7 +133,7 @@ class EnhancedChangesListTest extends MediaWikiLangTestCase {
        private function getEditChange( $timestamp ) {
                $user = $this->getMutableTestUser()->getUser();
                $recentChange = $this->testRecentChangesHelper->makeEditRecentChange(
-                       $user, 'Cat', $timestamp, 5, 191, 190, 0, 0
+                       $user, 'Cat', 0, 5, 191, $timestamp, 0, 0
                );
 
                return $recentChange;
index f892eb7..90c60c8 100644 (file)
@@ -126,9 +126,9 @@ class OldChangesListTest extends MediaWikiLangTestCase {
                $oldChangesList = $this->getOldChangesList();
                $line = $oldChangesList->recentChangesLine( $recentChange, false, 1 );
 
-               $this->assertRegExp( '/<li data-mw-revid="\d+" class="[\w\s-]*mw-tag-vandalism[\w\s-]*">/',
+               $this->assertRegExp( '/<li data-mw-revid="\d+" data-mw-ts="\d+" class="[\w\s-]*mw-tag-vandalism[\w\s-]*">/',
                        $line );
-               $this->assertRegExp( '/<li data-mw-revid="\d+" class="[\w\s-]*mw-tag-newbie[\w\s-]*">/',
+               $this->assertRegExp( '/<li data-mw-revid="\d+" data-mw-ts="\d+" class="[\w\s-]*mw-tag-newbie[\w\s-]*">/',
                        $line );
        }