Skip to content

Commit

Permalink
feat: in app banner notifications and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
abose committed Nov 18, 2023
1 parent 0a5e820 commit 08e074a
Show file tree
Hide file tree
Showing 15 changed files with 634 additions and 3 deletions.
8 changes: 8 additions & 0 deletions src/assets/notifications/dev/root/banner.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"WELCOME_DEVELOPER": {
"DANGER_SHOW_ON_EVERY_BOOT" : false,
"HTML_CONTENT": "<div>Welcome to Phoenix Code dev community! Click here to <a class='notification_ack' href='https://discord.com/invite/rBpTBPttca'> chat with our Discord Community.</a></div>",
"FOR_VERSIONS": ">=3.0.0",
"PLATFORM" : "all"
}
}
2 changes: 2 additions & 0 deletions src/assets/notifications/dev/root/toast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{
}
3 changes: 2 additions & 1 deletion src/brackets.config.dist.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"coreAnalyticsAppName" : "phoenix-prod",
"environment" : "production",
"buildtype" : "production",
"bugsnagEnv" : "production"
"bugsnagEnv" : "production",
"app_notification_url" : "https://updates.phcode.io/appNotifications/prod/"
}
3 changes: 2 additions & 1 deletion src/brackets.config.staging.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"coreAnalyticsAppName" : "phoenix-stage",
"environment" : "stage",
"buildtype" : "staging",
"bugsnagEnv" : "staging"
"bugsnagEnv" : "staging",
"app_notification_url" : "https://updates.phcode.io/appNotifications/staging/"
}
1 change: 1 addition & 0 deletions src/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"extension_registry_popularity": "https://extensions.phcode.dev/popularity.json",
"extension_url": "https://extensions.phcode.dev/extensions/",
"extension_store_url": "https://store.core.ai/src/",
"app_notification_url": "assets/notifications/dev/",
"linting.enabled_by_default": true,
"build_timestamp": "",
"googleAnalyticsID": "G-P4HJFPDB76",
Expand Down
227 changes: 227 additions & 0 deletions src/extensionsIntegrated/InAppNotifications/banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Copyright (c) 2019 - present Adobe. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/*global Phoenix*/
/**
* module for displaying in-app banner notifications
*
*/
define(function (require, exports, module) {

const AppInit = require("utils/AppInit"),
PreferencesManager = require("preferences/PreferencesManager"),
ExtensionUtils = require("utils/ExtensionUtils"),
Metrics = require("utils/Metrics"),
utils = require("./utils"),
NotificationBarHtml = require("text!./htmlContent/notificationContainer.html");

ExtensionUtils.loadStyleSheet(module, "styles/styles.css");

// duration of one day in milliseconds
const ONE_DAY = 1000 * 60 * 60 * 24;
const IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE = "InAppNotificationsBannerShown";
const NOTIFICATION_ACK_CLASS = "notification_ack";

// Init default last notification number
PreferencesManager.stateManager.definePreference(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE,
"object", {});

/**
* If there are multiple notifications, thew will be shown one after the other and not all at once.
* A sample notifications is as follows:
* {
* "SAMPLE_NOTIFICATION_NAME": {
* "DANGER_SHOW_ON_EVERY_BOOT" : false,
* "HTML_CONTENT": "<div>hello world <a class='notification_ack'>Click to acknowledge.</a></div>",
* "FOR_VERSIONS": "1.x || >=2.5.0 || 5.0.0 - 7.2.3",
* "PLATFORM" : "allDesktop"
* },
* "ANOTHER_SAMPLE_NOTIFICATION_NAME": {etc}
* }
* By default, a notification is shown only once except if `DANGER_SHOW_ON_EVERY_BOOT` is set
* or there is an html element with class `notification_ack`.
*
* 1. `SAMPLE_NOTIFICATION_NAME` : This is a unique ID. It is used to check if the notification was shown to user.
* 2. `DANGER_SHOW_ON_EVERY_BOOT` : (Default false) Setting this to true will cause the
* notification to be shown on every boot. This is bad ux and only be used if there is a critical security issue
* that we want the version not to be used.
* 3. `HTML_CONTENT`: The actual html content to show to the user. It can have an optional `notification_ack` class.
* Setting this class will cause the notification to be shown once a day until the user explicitly clicks
* on any html element with class `notification_ack` or explicitly click the close button.
* If such a class is not present, then the notification is shown only once ever.
* 4. `FOR_VERSIONS` : [Semver compatible version filter](https://www.npmjs.com/package/semver).
* The notification will be shown to all versions satisfying this.
* 5. `PLATFORM`: A comma seperated list of all platforms in which the message will be shown.
* allowed values are: `mac,win,linux,allDesktop,firefox,chrome,safari,allBrowser,all`
* @param notifications
* @returns {false|*}
* @private
*/
async function _renderNotifications(notifications) {
if(!notifications) {
return; // nothing to show here
}

const _InAppBannerShownAndDone = PreferencesManager.getViewState(
IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE);

for(const notificationID of Object.keys(notifications)){
if(!_InAppBannerShownAndDone[notificationID]) {
const notification = notifications[notificationID];
if(!utils.isValidForThisVersion(notification.FOR_VERSIONS)){
continue;
}
if(!utils.isValidForThisPlatform(notification.PLATFORM)){
continue;
}
if(!notification.HTML_CONTENT.includes(NOTIFICATION_ACK_CLASS)
&& !notification.DANGER_SHOW_ON_EVERY_BOOT){
// One time notification. mark as shown and never show again
_markAsShownAndDone(notificationID);
}
await showBannerAndWaitForDismiss(notification.HTML_CONTENT, notificationID, notification);
if(!notification.DANGER_SHOW_ON_EVERY_BOOT){
_markAsShownAndDone(notificationID);
}
}
}
}

function _markAsShownAndDone(notificationID) {
const _InAppBannersShownAndDone = PreferencesManager.getViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE);
_InAppBannersShownAndDone[notificationID] = true;
PreferencesManager.setViewState(IN_APP_NOTIFICATIONS_BANNER_SHOWN_STATE,
_InAppBannersShownAndDone);
}

function fetchJSON(url) {
return fetch(url)
.then(response => {
if (!response.ok) {
return null;
}
return response.json();
});
}

let inProgress = false;
function _fetchAndRenderNotifications() {
if(inProgress){
return;
}
inProgress = true;
const locale = brackets.getLocale(); // en-US default
const fetchURL = `${brackets.config.app_notification_url}${locale}/banner.json`;
const defaultFetchURL = `${brackets.config.app_notification_url}root/banner.json`;
// Fetch data from fetchURL first
fetchJSON(fetchURL)
.then(fetchedJSON => {
// Check if fetchedJSON is empty or undefined
if (fetchedJSON === null) {
// Fetch data from defaultFetchURL if fetchURL didn't provide data
return fetchJSON(defaultFetchURL);
}
return fetchedJSON;
})
.then(_renderNotifications) // Call the render function with the fetched JSON data
.catch(error => {
console.error(`Error fetching and rendering banner.json`, error);
})
.finally(()=>{
inProgress = false;
});
}


/**
* Removes and cleans up the notification bar from DOM
*/
function cleanNotificationBanner() {
const $notificationBar = $('#notification-bar');
if ($notificationBar.length > 0) {
$notificationBar.remove();
}
}

/**
* Displays the Notification Bar UI
*
*/
async function showBannerAndWaitForDismiss(html, notificationID) {
let resolved = false;
return new Promise((resolve)=>{
const $htmlContent = $(html),
$notificationBarElement = $(NotificationBarHtml);

// Remove any SCRIPT tag to avoid secuirity issues
$htmlContent.find('script').remove();

// Remove any STYLE tag to avoid styling impact on Brackets DOM
$htmlContent.find('style').remove();

cleanNotificationBanner(); //Remove an already existing notification bar, if any
$notificationBarElement.prependTo(".content");

var $notificationBar = $('#notification-bar'),
$notificationContent = $notificationBar.find('.content-container'),
$closeIcon = $notificationBar.find('.close-icon');

$notificationContent.append($htmlContent);
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
"shown");

// Click handlers on actionable elements
if ($closeIcon.length > 0) {
$closeIcon.click(function () {
cleanNotificationBanner();
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
"closeClick");
!resolved && resolve($htmlContent);
resolved = true;
});
}

$notificationBar.find(`.${NOTIFICATION_ACK_CLASS}`).click(function() {
// Your click event handler logic here
cleanNotificationBanner();
Metrics.countEvent(Metrics.EVENT_TYPE.NOTIFICATIONS, "banner-"+notificationID,
"ackClick");
!resolved && resolve($htmlContent);
resolved = true;
});
});
}


AppInit.appReady(function () {
if(Phoenix.isTestWindow) {
return;
}
_fetchAndRenderNotifications();
setInterval(_fetchAndRenderNotifications, ONE_DAY);
});

if(Phoenix.isTestWindow){
exports.cleanNotificationBanner = cleanNotificationBanner;
exports._renderNotifications = _renderNotifications;
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div id="notification-bar" tabindex="0">
<div class="content-container">
</div>
<div class="close-icon-container">
<button type="button" class="close-icon" tabIndex="0">&times;</button>
</div>
</div>
29 changes: 29 additions & 0 deletions src/extensionsIntegrated/InAppNotifications/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 - present Adobe. All rights reserved.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
*/
/**
* module for displaying in-app notifications
*
*/
define(function (require, exports, module) {
require("./banner");
});
56 changes: 56 additions & 0 deletions src/extensionsIntegrated/InAppNotifications/styles/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#notification-bar {
display: block;
background-color: #105F9C;
box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.53);
padding: 5px 0px;
width: 100%;
min-height: 39px;
position: absolute;
z-index: 16;
left: 0px;
bottom: 25px;
outline: none;
overflow: hidden;
color: rgb(51, 51, 51);
background: rgb(223, 226, 226);
}

.dark #notification-bar {
color: #ccc;
background: #2c2c2c;
}

#notification-bar .content-container {
padding: 5px 10px;
float: left;
width: 100%;
}

#notification-bar .close-icon-container {
height: auto;
position: absolute;
float: right;
text-align: center;
width: auto;
min-width: 66px;
right: 20px;
top: 10px;
}

#notification-bar .close-icon-container .close-icon {
display: block;
font-size: 18px;
line-height: 18px;
text-decoration: none;
width: 18px;
height: 18px;
background-color: transparent;
border: none;
padding: 0px; /*This is needed to center the icon*/
float: right;
}

.dark #notification-bar .close-icon-container .close-icon {
color: #ccc;
}

Loading

0 comments on commit 08e074a

Please sign in to comment.