Skip to content
This repository was archived by the owner on Sep 6, 2021. It is now read-only.

Commit ee01b79

Browse files
swmitrashubhsnov
authored andcommitted
InApp Notifications (#14715)
* InApp Notification * Minor cleanup * Remove template parameters * MIn height for notification bar * Address code review comments
1 parent 83eeae5 commit ee01b79

6 files changed

Lines changed: 390 additions & 0 deletions

File tree

src/brackets.config.dev.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"serviceKey" : "brackets-service",
55
"environment" : "stage",
66
"update_info_url" : "https://s3.amazonaws.com/files.brackets.io/updates/prerelease/<locale>.json",
7+
"notification_info_url" : "https://s3.amazonaws.com/files.brackets.io/notifications/prerelease/<locale>.json",
78
"buildtype" : "dev"
89
}

src/brackets.config.dist.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"serviceKey" : "brackets-service",
55
"environment" : "production",
66
"update_info_url" : "https://getupdates.brackets.io/getupdates/",
7+
"notification_info_url" : "https://getupdates.brackets.io/getnotifications?locale=<locale>",
78
"buildtype" : "production"
89
}

src/brackets.config.prerelease.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@
44
"serviceKey" : "brackets-service",
55
"environment" : "production",
66
"update_info_url" : "https://s3.amazonaws.com/files.brackets.io/updates/prerelease/<locale>.json",
7+
"notification_info_url" : "https://s3.amazonaws.com/files.brackets.io/notifications/prerelease/<locale>.json",
78
"buildtype" : "prerelease"
89
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<div id="notification-bar" tabindex="0">
2+
<div class="content-container">
3+
</div>
4+
<div class="close-icon-container">
5+
<button type="button" class="close-icon" tabIndex="0">&times;</button>
6+
</div>
7+
</div>
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/*
2+
* Copyright (c) 2019 - present Adobe. All rights reserved.
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a
5+
* copy of this software and associated documentation files (the "Software"),
6+
* to deal in the Software without restriction, including without limitation
7+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
8+
* and/or sell copies of the Software, and to permit persons to whom the
9+
* Software is furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20+
* DEALINGS IN THE SOFTWARE.
21+
*
22+
*/
23+
24+
/**
25+
* module for displaying in-app notifications
26+
*
27+
*/
28+
define(function (require, exports, module) {
29+
"use strict";
30+
31+
var AppInit = brackets.getModule("utils/AppInit"),
32+
PreferencesManager = brackets.getModule("preferences/PreferencesManager"),
33+
ExtensionUtils = brackets.getModule("utils/ExtensionUtils"),
34+
ExtensionManager = brackets.getModule("extensibility/ExtensionManager"),
35+
HealthLogger = brackets.getModule("utils/HealthLogger"),
36+
NotificationBarHtml = require("text!htmlContent/notificationContainer.html");
37+
38+
ExtensionUtils.loadStyleSheet(module, "styles/styles.css");
39+
40+
// duration of one day in milliseconds
41+
var ONE_DAY = 1000 * 60 * 60 * 24;
42+
43+
// Init default last notification number
44+
PreferencesManager.stateManager.definePreference("lastHandledNotificationNumber", "number", 0);
45+
46+
// Init default last info URL fetch time
47+
PreferencesManager.stateManager.definePreference("lastNotificationURLFetchTime", "number", 0);
48+
49+
/**
50+
* Constructs notification info URL for XHR
51+
*
52+
* @param {string=} localeParam - optional locale, defaults to 'brackets.getLocale()' when omitted.
53+
* @returns {string} the new notification info url
54+
*/
55+
function _getVersionInfoUrl(localeParam) {
56+
57+
var locale = localeParam || brackets.getLocale();
58+
59+
if (locale.length > 2) {
60+
locale = locale.substring(0, 2);
61+
}
62+
63+
return brackets.config.notification_info_url.replace("<locale>", locale);
64+
}
65+
66+
/**
67+
* Get a data structure that has information for all Brackets targeted notifications.
68+
*
69+
* _notificationInfoUrl is used for unit testing.
70+
*/
71+
function _getNotificationInformation(_notificationInfoUrl) {
72+
// Last time the versionInfoURL was fetched
73+
var lastInfoURLFetchTime = PreferencesManager.getViewState("lastNotificationURLFetchTime");
74+
75+
var result = new $.Deferred();
76+
var fetchData = false;
77+
var data;
78+
79+
// If we don't have data saved in prefs, fetch
80+
data = PreferencesManager.getViewState("notificationInfo");
81+
if (!data) {
82+
fetchData = true;
83+
}
84+
85+
// If more than 24 hours have passed since our last fetch, fetch again
86+
if (Date.now() > lastInfoURLFetchTime + ONE_DAY) {
87+
fetchData = true;
88+
}
89+
90+
if (fetchData) {
91+
var lookupPromise = new $.Deferred(),
92+
localNotificationInfoUrl;
93+
94+
// If the current locale isn't "en" or "en-US", check whether we actually have a
95+
// locale-specific notification target, and fall back to "en" if not.
96+
var locale = brackets.getLocale().toLowerCase();
97+
if (locale !== "en" && locale !== "en-us") {
98+
localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl();
99+
// Check if we can reach a locale specific notifications source
100+
$.ajax({
101+
url: localNotificationInfoUrl,
102+
cache: false,
103+
type: "HEAD"
104+
}).fail(function (jqXHR, status, error) {
105+
// Fallback to "en" locale
106+
localNotificationInfoUrl = _getVersionInfoUrl("en");
107+
}).always(function (jqXHR, status, error) {
108+
lookupPromise.resolve();
109+
});
110+
} else {
111+
localNotificationInfoUrl = _notificationInfoUrl || _getVersionInfoUrl("en");
112+
lookupPromise.resolve();
113+
}
114+
115+
lookupPromise.done(function () {
116+
$.ajax({
117+
url: localNotificationInfoUrl,
118+
dataType: "json",
119+
cache: false
120+
}).done(function (notificationInfo, textStatus, jqXHR) {
121+
lastInfoURLFetchTime = (new Date()).getTime();
122+
PreferencesManager.setViewState("lastNotificationURLFetchTime", lastInfoURLFetchTime);
123+
PreferencesManager.setViewState("notificationInfo", notificationInfo);
124+
result.resolve(notificationInfo);
125+
}).fail(function (jqXHR, status, error) {
126+
// When loading data for unit tests, the error handler is
127+
// called but the responseText is valid. Try to use it here,
128+
// but *don't* save the results in prefs.
129+
130+
if (!jqXHR.responseText) {
131+
// Text is NULL or empty string, reject().
132+
result.reject();
133+
return;
134+
}
135+
136+
try {
137+
data = JSON.parse(jqXHR.responseText);
138+
result.resolve(data);
139+
} catch (e) {
140+
result.reject();
141+
}
142+
});
143+
});
144+
} else {
145+
result.resolve(data);
146+
}
147+
148+
return result.promise();
149+
}
150+
151+
152+
/**
153+
* Check for notifications, notification overlays are always displayed
154+
*
155+
* @return {$.Promise} jQuery Promise object that is resolved or rejected after the notification check is complete.
156+
*/
157+
function checkForNotification(versionInfoUrl) {
158+
var result = new $.Deferred();
159+
160+
_getNotificationInformation(versionInfoUrl)
161+
.done(function (notificationInfo) {
162+
// Get all available notifications
163+
var notifications = notificationInfo.notifications;
164+
if (notifications && notifications.length > 0) {
165+
// Iterate through notifications and act only on the most recent
166+
// applicable notification
167+
notifications.every(function(notificationObj) {
168+
// Only show the notification overlay if the user hasn't been
169+
// alerted of this notification
170+
if (_checkNotificationValidity(notificationObj)) {
171+
if (notificationObj.silent) {
172+
// silent notifications, to gather user validity based on filters
173+
HealthLogger.sendAnalyticsData("notification", notificationObj.sequence, "handled");
174+
} else {
175+
showNotification(notificationObj);
176+
}
177+
// Break, we have acted on one notification already
178+
return false;
179+
}
180+
// Continue, we haven't yet got a notification to act on
181+
return true;
182+
});
183+
}
184+
result.resolve();
185+
})
186+
.fail(function () {
187+
// Error fetching the update data. If this is a forced check, alert the user
188+
result.reject();
189+
});
190+
191+
return result.promise();
192+
}
193+
194+
function _checkPlatform(filters, _platform) {
195+
return !filters.platforms || filters.platforms.length === 0 || filters.platforms.indexOf(_platform) >=0;
196+
}
197+
198+
function _checkBuild(filters, _build) {
199+
return !filters.builds || filters.builds.length === 0 || filters.builds.indexOf(_build) >=0;
200+
}
201+
202+
function _checkVersion(filters, _version) {
203+
var re = new RegExp(filters.version);
204+
return re.exec(_version);
205+
}
206+
207+
function _checkLocale(filters, _locale) {
208+
return !filters.locales || filters.locales.length === 0 || filters.locales.indexOf(_locale) >=0;
209+
}
210+
211+
function _checkExpiry(expiry) {
212+
return Date.now() <= expiry;
213+
}
214+
215+
function _checkExtensions(filters) {
216+
var allExtensions = ExtensionManager.extensions,
217+
allExtnsMatched = true,
218+
userExtensionKeys = Object.keys(allExtensions).filter(function(k) {
219+
return allExtensions[k].installInfo.locationType === 'user';
220+
});
221+
222+
if (!filters.extensions) {
223+
allExtnsMatched = userExtensionKeys.size === 0;
224+
} else if (filters.extensions.length === 0) {
225+
allExtnsMatched = userExtensionKeys.length > 0;
226+
} else {
227+
var filteredExtns = filters.extensions,
228+
extnIterator = null;
229+
for (var i=0; i < filteredExtns.length; i++) {
230+
extnIterator = filteredExtns[i];
231+
if (userExtensionKeys.indexOf(extnIterator) === -1) {
232+
allExtnsMatched = false;
233+
break;
234+
}
235+
}
236+
}
237+
return allExtnsMatched;
238+
}
239+
240+
function _checkNotificationValidity(notificationObj) {
241+
242+
var filters = notificationObj.filters,
243+
_platform = brackets.getPlatformInfo(),
244+
_locale = brackets.getLocale(),
245+
_lastHandledNotificationNumber = PreferencesManager.getViewState("lastHandledNotificationNumber"),
246+
// Extract current build number from package.json version field 0.0.0-0
247+
_buildNumber = Number(/-([0-9]+)/.exec(brackets.metadata.version)[1]),
248+
_version = brackets.metadata.apiVersion;
249+
250+
if(_locale.length > 2) {
251+
_locale = _locale.substring(0, 2);
252+
}
253+
254+
return notificationObj.sequence > _lastHandledNotificationNumber
255+
&& _checkExpiry(notificationObj.expiry)
256+
&& _checkPlatform(filters, _platform)
257+
&& _checkLocale(filters, _locale)
258+
&& _checkVersion(filters, _version)
259+
&& _checkBuild(filters, _buildNumber)
260+
&& _checkExtensions(filters);
261+
}
262+
263+
264+
/**
265+
* Removes and cleans up the notification bar from DOM
266+
*/
267+
function cleanNotificationBar() {
268+
var $notificationBar = $('#notification-bar');
269+
if ($notificationBar.length > 0) {
270+
$notificationBar.remove();
271+
}
272+
}
273+
274+
/**
275+
* Displays the Notification Bar UI
276+
* @param {object} msgObj - json object containing message info to be displayed
277+
*
278+
*/
279+
function showNotification(msgObj) {
280+
var $htmlContent = $(msgObj.html),
281+
$notificationBarElement = $(NotificationBarHtml);
282+
283+
// Remove any SCRIPT tag to avoid secuirity issues
284+
$htmlContent.find('script').remove();
285+
286+
// Remove any STYLE tag to avoid styling impact on Brackets DOM
287+
$htmlContent.find('style').remove();
288+
289+
cleanNotificationBar(); //Remove an already existing notification bar, if any
290+
$notificationBarElement.prependTo(".content");
291+
292+
var $notificationBar = $('#notification-bar'),
293+
$notificationContent = $notificationBar.find('.content-container'),
294+
$closeIcon = $notificationBar.find('.close-icon');
295+
296+
$notificationContent.append($htmlContent);
297+
HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "shown");
298+
299+
// Click handlers on actionable elements
300+
if ($closeIcon.length > 0) {
301+
$closeIcon.click(function () {
302+
cleanNotificationBar();
303+
PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence);
304+
HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedByClose");
305+
});
306+
}
307+
308+
if (msgObj.actionables) {
309+
$(msgObj.actionables).click(function () {
310+
cleanNotificationBar();
311+
PreferencesManager.setViewState("lastHandledNotificationNumber", msgObj.sequence);
312+
HealthLogger.sendAnalyticsData("notification", msgObj.sequence, "dismissedBy" + this.id);
313+
});
314+
}
315+
}
316+
317+
318+
AppInit.appReady(function () {
319+
checkForNotification();
320+
});
321+
322+
// For unit tests only
323+
exports.checkForNotification = checkForNotification;
324+
});

0 commit comments

Comments
 (0)