summaryrefslogtreecommitdiff
blob: 407bfd41c69c5cc1b7770ea22a87ee95c2ab6e98 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/* 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';

	// Remove ?markasread=XYZ from the URL
	var uri = new mw.Uri();
	if ( uri.query.markasread !== undefined ) {
		delete uri.query.markasread;
		delete uri.query.markasreadwiki;
		window.history.replaceState( null, document.title, uri );
	}

	// 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();
	}
} );