mediawiki-extensions-Echo/modules/ext.echo.init.js
jdlrobson 31401f9b00 Catch malformed URIs
It is documented that Uri can throw an exception so this should guard
against this case.

Bug: T261799
Change-Id: Ia45654194f853923a5b05ceff3ab2b47f6dd87d8
2020-10-20 19:42:20 +00:00

317 lines
11 KiB
JavaScript

/* eslint-disable no-jquery/no-global-selector */
mw.echo = mw.echo || {};
mw.echo.config = mw.echo.config || {};
// Set default max prioritized action links per item
mw.echo.config.maxPrioritizedActions = 2;
/**
* Initialise desktop Echo experience
*/
function initDesktop() {
'use strict';
var uri;
// Remove ?markasread=XYZ from the URL
try {
uri = new mw.Uri();
if ( uri.query.markasread !== undefined ) {
delete uri.query.markasread;
delete uri.query.markasreadwiki;
window.history.replaceState( null, document.title, uri );
}
} catch ( e ) {
// Catch problems when the URI is malformed (T261799)
// e.g. #/media/Fitxer:Campbells_Soup_Cans_MOMA_reduced_80%.jpg
}
// Activate ooui
$( function () {
var selectedWidget,
echoApi,
messageController,
alertController,
messageModelManager,
alertModelManager,
unreadMessageCounter,
unreadAlertCounter,
maxNotificationCount = require( './config.json' ).EchoMaxNotificationCount,
pollingRate = require( './config.json' ).EchoPollForUpdates,
documentTitle = document.title,
$existingAlertLink = $( '#pt-notifications-alert a' ),
$existingMessageLink = $( '#pt-notifications-notice a' ),
numAlerts = $existingAlertLink.attr( 'data-counter-num' ),
numMessages = $existingMessageLink.attr( 'data-counter-num' ),
badgeLabelAlerts = $existingAlertLink.attr( 'data-counter-text' ),
badgeLabelMessages = $existingMessageLink.attr( 'data-counter-text' ),
// eslint-disable-next-line no-jquery/no-class-state
hasUnseenAlerts = $existingAlertLink.hasClass( 'mw-echo-unseen-notifications' ),
// eslint-disable-next-line no-jquery/no-class-state
hasUnseenMessages = $existingMessageLink.hasClass( 'mw-echo-unseen-notifications' ),
// latestMessageNotifTime is the time of most recent notification that came when we called showNotificationSnippet last
// the function showNotificationSnippet returns the time of the latest notification and latestMessageNotifTime is updated
latestMessageNotifTime = new Date(),
latestAlertNotifTime = new Date(),
alertCount = parseInt( numAlerts ),
messageCount = parseInt( numMessages ),
loadingPromise = null,
// Store links
links = {
notifications: $existingAlertLink.attr( 'href' ) || mw.util.getUrl( 'Special:Notifications' ),
preferences: ( $( '#pt-preferences a' ).attr( 'href' ) || mw.util.getUrl( 'Special:Preferences' ) ) +
'#mw-prefsection-echo'
};
function updateDocumentTitleWithNotificationCount( totalAlertCount, totalMessageCount ) {
var totalCount = totalAlertCount + totalMessageCount,
convertedTotalCount,
newTitle = documentTitle;
if ( totalCount > 0 ) {
convertedTotalCount = totalCount <= maxNotificationCount ? totalCount : maxNotificationCount + 1;
convertedTotalCount = mw.msg( 'echo-badge-count', mw.language.convertNumber( convertedTotalCount ) );
newTitle = mw.msg( 'parentheses', convertedTotalCount ) + ' ' + documentTitle;
}
document.title = newTitle;
}
/**
* Show notification snippet via mw.notify of notifications which came after highestNotifTime.
*
* @param {mw.echo.dm.ModelManager} modelManager
* @param {Date} highestNotifTime Timestamp of latest notification the last time function was called
* @return {Date} Timestamp of latest notification
*/
function showNotificationSnippet( modelManager, highestNotifTime ) {
var timestampAsDate,
highestTime = new Date();
highestTime = highestNotifTime;
modelManager.getLocalNotifications().forEach( function ( notificationItem ) {
timestampAsDate = new Date( notificationItem.timestamp );
if ( timestampAsDate > highestNotifTime ) {
if ( timestampAsDate > highestTime ) {
highestTime = timestampAsDate;
}
if ( !notificationItem.seen ) {
mw.notify( $.parseHTML( notificationItem.content.header ), { title: mw.msg( 'echo-displaysnippet-title' ) } );
}
}
}
);
return highestTime;
}
/**
* Change the seen state of badges if there are any unseen notifications.
*
* @param {mw.echo.dm.ModelManager} modelManager
* @param {mw.echo.ui.NotificationBadgeWidget} badgeWidget
*/
function updateBadgeState( modelManager, badgeWidget ) {
modelManager.getLocalNotifications().forEach( function ( notificationItem ) {
if ( !notificationItem.isSeen() ) {
badgeWidget.updateBadgeSeenState( true );
}
} );
}
function isLivePollingFeatureEnabledOnWiki() {
return pollingRate !== 0;
}
/**
* User has opted in to preference to show notification snippets and update document title with unread count.
*
* Only useful when isLivePollingFeatureEnabledOnWiki() returns true.
*
* @return {boolean} User preference
*/
function userHasOptedInToLiveNotifications() {
return mw.user.options.get( 'echo-show-poll-updates' ) === '1';
}
// Change document title on initialization only when polling rate feature flag is non-zero.
if ( isLivePollingFeatureEnabledOnWiki() && userHasOptedInToLiveNotifications() ) {
updateDocumentTitleWithNotificationCount( alertCount, messageCount );
}
function loadEcho() {
if ( loadingPromise !== null ) {
return loadingPromise;
}
// This part executes only once, either when header icons are clicked or after completion of 60secs whichever occur first.
echoApi = new mw.echo.api.EchoApi();
loadingPromise = mw.loader.using( 'ext.echo.ui.desktop' ).then( function () {
// Overlay
mw.echo.ui.$overlay.appendTo( document.body );
unreadAlertCounter = new mw.echo.dm.UnreadNotificationCounter( echoApi, 'alert', maxNotificationCount );
alertModelManager = new mw.echo.dm.ModelManager( unreadAlertCounter, { type: 'alert' } );
alertController = new mw.echo.Controller( echoApi, alertModelManager );
mw.echo.ui.alertWidget = new mw.echo.ui.NotificationBadgeWidget(
alertController,
alertModelManager,
links,
{
numItems: Number( numAlerts ),
convertedNumber: badgeLabelAlerts,
hasUnseen: hasUnseenAlerts,
badgeIcon: 'bell',
$overlay: mw.echo.ui.$overlay,
href: $existingAlertLink.attr( 'href' )
}
);
// Replace the link button with the ooui button
$existingAlertLink.parent().replaceWith( mw.echo.ui.alertWidget.$element );
alertModelManager.on( 'allTalkRead', function () {
// If there was a talk page notification, get rid of it
$( '#pt-mytalk a' )
.removeClass( 'mw-echo-alert' )
.text( mw.msg( 'mytalk' ) );
} );
// listen to event countChange and change title only if polling rate is non-zero
if ( isLivePollingFeatureEnabledOnWiki() ) {
alertModelManager.getUnreadCounter().on( 'countChange', function ( count ) {
alertController.fetchLocalNotifications().then( function () {
updateBadgeState( alertModelManager, mw.echo.ui.alertWidget );
if ( userHasOptedInToLiveNotifications() ) {
latestAlertNotifTime = showNotificationSnippet( alertModelManager, latestAlertNotifTime );
alertCount = count;
updateDocumentTitleWithNotificationCount( count, messageCount );
}
} );
} );
}
// Load message button and popup if messages exist
if ( $existingMessageLink.length ) {
unreadMessageCounter = new mw.echo.dm.UnreadNotificationCounter( echoApi, 'message', maxNotificationCount );
messageModelManager = new mw.echo.dm.ModelManager( unreadMessageCounter, { type: 'message' } );
messageController = new mw.echo.Controller( echoApi, messageModelManager );
mw.echo.ui.messageWidget = new mw.echo.ui.NotificationBadgeWidget(
messageController,
messageModelManager,
links,
{
$overlay: mw.echo.ui.$overlay,
numItems: Number( numMessages ),
hasUnseen: hasUnseenMessages,
badgeIcon: 'tray',
convertedNumber: badgeLabelMessages,
href: $existingMessageLink.attr( 'href' )
}
);
// Replace the link button with the ooui button
$existingMessageLink.parent().replaceWith( mw.echo.ui.messageWidget.$element );
// listen to event countChange and change title only if polling rate is non-zero
if ( isLivePollingFeatureEnabledOnWiki() ) {
messageModelManager.getUnreadCounter().on( 'countChange', function ( count ) {
messageController.fetchLocalNotifications().then( function () {
updateBadgeState( messageModelManager, mw.echo.ui.messageWidget );
if ( userHasOptedInToLiveNotifications() ) {
latestMessageNotifTime = showNotificationSnippet( messageModelManager, latestMessageNotifTime );
messageCount = count;
updateDocumentTitleWithNotificationCount( alertCount, count );
}
} );
} );
}
}
} );
return loadingPromise;
}
// Respond to click on the notification button and load the UI on demand
$( '.mw-echo-notification-badge-nojs' ).on( 'click', function ( e ) {
var timeOfClick = mw.now(),
$badge = $( this ),
clickedSection = $badge.parent().prop( 'id' ) === 'pt-notifications-alert' ? 'alert' : 'message';
if ( e.which !== 1 || $badge.data( 'clicked' ) ) {
return false;
}
$badge.data( 'clicked', true );
// Dim the badge while we load
$badge.addClass( 'mw-echo-notifications-badge-dimmed' );
// Fire the notification API requests
echoApi = new mw.echo.api.EchoApi();
echoApi.fetchNotifications( clickedSection )
.then( function ( data ) {
mw.track( 'timing.MediaWiki.echo.overlay.api', mw.now() - timeOfClick );
return data;
} );
loadEcho().then( function () {
// Now that the module loaded, show the popup
selectedWidget = clickedSection === 'alert' ? mw.echo.ui.alertWidget : mw.echo.ui.messageWidget;
selectedWidget.once( 'finishLoading', function () {
// Log timing after notifications are shown
mw.track( 'timing.MediaWiki.echo.overlay', mw.now() - timeOfClick );
} );
selectedWidget.popup.toggle( true );
mw.track( 'timing.MediaWiki.echo.overlay.ooui', mw.now() - timeOfClick );
if ( hasUnseenAlerts || hasUnseenMessages ) {
// Clicked on the flyout due to having unread notifications
// This is part of tracking how likely users are to click a badge with unseen notifications.
// The other part is the 'echo.unseen' counter, see EchoHooks::onPersonalUrls().
mw.track( 'counter.MediaWiki.echo.unseen.click' );
}
}, function () {
// Un-dim badge if loading failed
$badge.removeClass( 'mw-echo-notifications-badge-dimmed' );
} );
// Prevent default
return false;
} );
function pollForNotificationCountUpdates() {
alertController.refreshUnreadCount();
messageController.refreshUnreadCount();
// Make notification update after n*pollingRate(time in secs) where n depends on document.hidden
setTimeout( pollForNotificationCountUpdates, ( document.hidden ? 5 : 1 ) * pollingRate * 1000 );
}
function pollStart() {
if ( mw.config.get( 'skin' ) !== 'minerva' && isLivePollingFeatureEnabledOnWiki() ) {
// load widgets if not loaded already then start polling
loadEcho().then( pollForNotificationCountUpdates );
}
}
setTimeout( pollStart, 60 * 1000 );
} );
}
/**
* Initialise a mobile experience instead
*/
function initMobile() {
if ( !mw.user.isAnon() ) {
mw.loader.using( [ 'ext.echo.mobile', 'mobile.startup' ] ).then( function ( require ) {
require( 'ext.echo.mobile' )();
} );
}
}
$( function () {
if ( mw.config.get( 'wgMFMode' ) ) {
initMobile();
} else {
initDesktop();
}
} );