mediawiki-extensions-Popups/resources/ext.popups/actions.js
joakin efb7b3d872 Wire up saving enabled/disabled in settings dialog
The save action has been implemented, and it is listened to by the
canonical enabled state in the previews reducer, and by the settings
reducer to perform UI changes.

The enabled state of the application has been kept in the preview
reducer as the canonical source of truth. See supporting changes for
documentation about the decision.

Actions:
* Introduce new action SETTINGS_CHANGE with the enabled status
* Trigger that action when clicking Save in the settings dialog

Reducers:
* Listen to SETTINGS_CHANGE in the preview reducer to update enabled
  status
* On the settings reducer
  * Handle the SETTINGS_CHANGE action
  * Add showHelp flag to determine if the help should be showing

Change listeners:
* Switch to compare past vs present changes in the implementation
* Handle showing and hiding the help

Supporting changes:
* On the rendered settings dialog:
  * Change #hide to actually just hide and remove legacy if statement
  * Add #toggleHelp method to show or hide the help dialog
* Add doc/adr/0003-keep-enabled-state-only-in-preview-reducer.md to
  support the decision of making the saveSettings action creator return
  a Redux.Thunk and keeping the enabled state just in the preview
  reducer.
  * Add NB to actions#saveSettings explaining and linking to the
    document

Follow commits soon:
* Persist the settings change to local storage when it changes, and
  unify with the preview change listener

Change-Id: I80dc5f29fbe6286f2e3e3b50d909894bc5041ccd
2016-12-14 14:35:59 +00:00

304 lines
7.2 KiB
JavaScript

( function ( mw, $ ) {
var actions = {},
types = {
BOOT: 'BOOT',
LINK_DWELL: 'LINK_DWELL',
LINK_ABANDON_START: 'LINK_ABANDON_START',
LINK_ABANDON_END: 'LINK_ABANDON_END',
LINK_CLICK: 'LINK_CLICK',
FETCH_START: 'FETCH_START',
FETCH_END: 'FETCH_END',
FETCH_FAILED: 'FETCH_FAILED',
PREVIEW_DWELL: 'PREVIEW_DWELL',
PREVIEW_ABANDON_START: 'PREVIEW_ABANDON_START',
PREVIEW_ABANDON_END: 'PREVIEW_ABANDON_END',
PREVIEW_ANIMATING: 'PREVIEW_ANIMATING',
PREVIEW_INTERACTIVE: 'PREVIEW_INTERACTIVE',
PREVIEW_SHOW: 'PREVIEW_SHOW',
PREVIEW_CLICK: 'PREVIEW_CLICK',
SETTINGS_SHOW: 'SETTINGS_SHOW',
SETTINGS_HIDE: 'SETTINGS_HIDE',
SETTINGS_CHANGE: 'SETTINGS_CHANGE',
EVENT_LOGGED: 'EVENT_LOGGED'
},
FETCH_START_DELAY = 500, // ms.
ABANDON_END_DELAY = 300; // ms.
/**
* Mixes in timing information to an action.
*
* Warning: the `baseAction` parameter is modified and returned.
*
* @param {Object} baseAction
* @return {Object}
*/
function timedAction( baseAction ) {
baseAction.timestamp = mw.now();
return baseAction;
}
/**
* Represents Page Previews booting.
*
* When a Redux store is created, the `@@INIT` action is immediately
* dispatched to it. To avoid overriding the term, we refer to booting rather
* than initializing.
*
* Page Previews persists critical pieces of information to local storage.
* Since reading from and writing to local storage are synchronous, Page
* Previews is booted when the browser is idle (using
* [`mw.requestIdleCallback`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback))
* so as not to impact latency-critical events.
*
* @param {Function} isUserInCondition See `mw.popups.createExperiment`
* @param {mw.user} user
* @param {ext.popups.UserSettings} userSettings
* @param {Function} generateToken
* @param {mw.Map} config The config of the MediaWiki client-side application,
* i.e. `mw.config`
*/
actions.boot = function (
isUserInCondition,
user,
userSettings,
generateToken,
config
) {
var editCount = config.get( 'wgUserEditCount' ),
previewCount = userSettings.getPreviewCount();
return {
type: types.BOOT,
sessionToken: user.sessionId(),
pageToken: generateToken(),
page: {
title: config.get( 'wgTitle' ),
namespaceID: config.get( 'wgNamespaceNumber' ),
id: config.get( 'wgArticleId' )
},
user: {
isInCondition: isUserInCondition(),
isAnon: user.isAnon(),
editCount: editCount,
previewCount: previewCount
}
};
};
/**
* Represents Page Previews fetching data via the [gateway](./gateway.js).
*
* @param {ext.popups.Gateway} gateway
* @param {Element} el
* @return {Redux.Thunk}
*/
function fetch( gateway, el ) {
var title = $( el ).data( 'page-previews-title' );
return function ( dispatch ) {
dispatch( {
type: types.FETCH_START,
el: el,
title: title
} );
gateway( title )
.fail( function () {
dispatch( {
type: types.FETCH_FAILED,
el: el
} );
} )
.done( function ( result ) {
dispatch( {
type: types.FETCH_END,
el: el,
result: result
} );
} );
};
}
/**
* Represents the user dwelling on a link, either by hovering over it with
* their mouse or by focussing it using their keyboard or an assistive device.
*
* @param {Element} el
* @param {Event} event
* @param {ext.popups.Gateway} gateway
* @param {Function} generateToken
* @return {Redux.Thunk}
*/
actions.linkDwell = function ( el, event, gateway, generateToken ) {
var interactionToken = generateToken();
return function ( dispatch, getState ) {
dispatch( timedAction( {
type: types.LINK_DWELL,
el: el,
event: event,
interactionToken: interactionToken
} ) );
mw.popups.wait( FETCH_START_DELAY )
.then( function () {
var previewState = getState().preview;
if ( previewState.enabled && previewState.activeLink === el ) {
dispatch( fetch( gateway, el ) );
}
} );
};
};
/**
* Represents the user abandoning a link, either by moving their mouse away
* from it or by shifting focus to another UI element using their keyboard or
* an assistive device.
*
* @param {Element} el
* @return {Object}
*/
actions.linkAbandon = function ( el ) {
return function ( dispatch ) {
dispatch( timedAction( {
type: types.LINK_ABANDON_START,
el: el
} ) );
mw.popups.wait( ABANDON_END_DELAY )
.then( function () {
dispatch( {
type: types.LINK_ABANDON_END,
el: el
} );
} );
};
};
/**
* Represents the user clicking on a link with their mouse, keyboard, or an
* assistive device.
*
* @param {Element} el
* @return {Object}
*/
actions.linkClick = function ( el ) {
return timedAction( {
type: types.LINK_CLICK,
el: el
} );
};
/**
* Represents the user dwelling on a preview with their mouse.
*
* @return {Object}
*/
actions.previewDwell = function () {
return {
type: types.PREVIEW_DWELL
};
};
/**
* Represents the user abandoning a preview by moving their mouse away from
* it.
*
* @return {Object}
*/
actions.previewAbandon = function () {
return function ( dispatch ) {
dispatch( {
type: types.PREVIEW_ABANDON_START
} );
mw.popups.wait( ABANDON_END_DELAY )
.then( function () {
dispatch( timedAction( {
type: types.PREVIEW_ABANDON_END
} ) );
} );
};
};
/**
* Represents a preview being shown to the user.
*
* This action is dispatched by the `mw.popups.changeListeners.render` change
* listener.
*
* @return {Object}
*/
actions.previewShow = function () {
return timedAction( {
type: types.PREVIEW_SHOW
} );
};
/**
* Represents the user clicking either the "Enable previews" footer menu link,
* or the "cog" icon that's present on each preview.
*
* @return {Object}
*/
actions.showSettings = function () {
return {
type: types.SETTINGS_SHOW
};
};
/**
* Represents the user closing the settings dialog and saving their settings.
*
* @return {Object}
*/
actions.hideSettings = function () {
return {
type: types.SETTINGS_HIDE
};
};
/**
* Represents the user saving their settings.
*
* N.B. This action returns a Redux.Thunk not because it needs to perform
* asynchronous work, but because it needs to query the global state for the
* current enabled state. In order to keep the enabled state in a single
* place (the preview reducer), we query it and dispatch it as `wasEnabled`
* so that other reducers (like settings) can act on it without having to
* duplicate the `enabled` state locally.
* See doc/adr/0003-keep-enabled-state-only-in-preview-reducer.md for more
* details.
*
* @return {Redux.Thunk}
*/
actions.saveSettings = function ( enabled ) {
return function ( dispatch, getState ) {
dispatch( {
type: types.SETTINGS_CHANGE,
wasEnabled: getState().preview.enabled,
enabled: enabled
} );
};
};
/**
* Represents the queued event being logged
* `mw.popups.changeListeners.eventLogging` change listener.
*
* @return {Object}
*/
actions.eventLogged = function () {
return {
type: types.EVENT_LOGGED
};
};
mw.popups.actions = actions;
mw.popups.actionTypes = types;
}( mediaWiki, jQuery ) );