Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/CONST.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const CLOUDFRONT_URL = 'https://d2k5nsl2zxldvw.cloudfront.net';

const CONST = {
CLOUDFRONT_URL: 'https://d2k5nsl2zxldvw.cloudfront.net',
CLOUDFRONT_URL,
EXPENSIFY_ICON_URL: `${CLOUDFRONT_URL}/images/favicon-2019.png`,
};

export default CONST;
126 changes: 126 additions & 0 deletions src/lib/Notification/BrowserNotifications.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Web and desktop implementation only. Do not import for direct use. Use Notification.

import Str from '../Str';
import CONST from '../../CONST';
import * as ActiveClientManager from '../ActiveClientManager';

const EXPENSIFY_ICON_URL = `${CONST.CLOUDFRONT_URL}/images/favicon-2019.png`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should move this to CONST.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah we probably should I'll fix this up.

const DEFAULT_DELAY = 4000;

/**
* Checks if the user has granted permission to show browser notifications
*
* @return {Promise}
*/
function canUseBrowserNotifications() {
return new Promise((resolve) => {
// They have no browser notifications so we can't use this feature
if (!window.Notification) {
return resolve(false);
}

// Check if they previously granted or denied us access to send a notification
const permissionGranted = Notification.permission === 'granted';

if (permissionGranted || Notification.permisson === 'denied') {
return resolve(permissionGranted);
}

// Check their global preferences for browser notifications and ask permission if they have none
Notification.requestPermission()
.then((status) => {
resolve(status === 'granted');
});
});
}

/**
* Light abstraction around browser push notifications.
* Checks for permission before determining whether to send.
*
* @param {Object} params
* @param {String} params.title
* @param {String} params.body
* @param {String} [params.icon] Default to Expensify logo
* @param {Number} [params.delay]
* @param {Function} [params.onClick]
* @param {String} [params.tag]
*
* @return {Promise} - resolves with Notification object or undefined
*/
function push({
title,
body,
delay = DEFAULT_DELAY,
onClick = () => {},
tag = '',
icon = EXPENSIFY_ICON_URL,
}) {
return new Promise((resolve) => {
if (!title || !body) {
throw new Error('BrowserNotification must include title and body parameter.');
}

canUseBrowserNotifications().then((canUseNotifications) => {
if (!canUseNotifications) {
resolve();
return;
}

const notification = new Notification(title, {
body,
icon,
tag,
});

// If we pass in a delay param greater than 0 the notification
// will auto-close after the specified time.
if (delay > 0) {
setTimeout(notification.close.bind(notification), delay);
}

notification.onclick = (event) => {
event.preventDefault();
onClick();
window.parent.focus();
window.focus();
notification.close();
};

resolve(notification);
});
});
}

/**
* BrowserNotification
* @namespace
*/
export default {
/**
* Create a report comment notification
*
* @param {Object} params
* @param {Object} params.reportAction
* @param {Function} params.onClick
*/
pushReportCommentNotification({reportAction, onClick}) {
if (!ActiveClientManager.isClientTheLeader()) {
return;
}

const {person, message} = reportAction;

const plainTextPerson = Str.htmlDecode(person.map(f => f.text).join());

// Specifically target the comment part of the message
const plainTextMessage = Str.htmlDecode((message.find(f => f.type === 'COMMENT') || {}).text);

push({
title: `New message from ${plainTextPerson}`,
body: plainTextMessage,
delay: 0,
onClick,
});
},
};
9 changes: 9 additions & 0 deletions src/lib/Notification/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import BrowserNotifications from './BrowserNotifications';

function showCommentNotification({reportAction, onClick}) {
BrowserNotifications.pushReportCommentNotification({reportAction, onClick});
}

export default {
showCommentNotification,
};
4 changes: 4 additions & 0 deletions src/lib/Notification/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Browser Notifications are not supported on mobile so we'll just noop here.
export default {
showCommentNotification: () => {},
};
32 changes: 30 additions & 2 deletions src/lib/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CONFIG from '../../CONFIG';
import * as Pusher from '../Pusher/pusher';
import promiseAllSettled from '../promiseAllSettled';
import ExpensiMark from '../ExpensiMark';
import Notification from '../Notification';
import * as PersonalDetails from './PersonalDetails';

/**
Expand All @@ -17,6 +18,7 @@ import * as PersonalDetails from './PersonalDetails';
* @param {object} reportAction
*/
function updateReportWithNewAction(reportID, reportAction) {
let currentUserEmail;
Ion.get(`${IONKEYS.REPORT}_${reportID}`, 'reportID')
.then((ionReportID) => {
// This is necessary for local development because there will be pusher events from other engineers with
Expand Down Expand Up @@ -46,8 +48,34 @@ function updateReportWithNewAction(reportID, reportAction) {
}))

// Put the report history back into Ion
.then((reportHistory) => {
Ion.set(`${IONKEYS.REPORT_HISTORY}_${reportID}`, reportHistory);
.then(reportHistory => Ion.set(`${IONKEYS.REPORT_HISTORY}_${reportID}`, reportHistory))

// Check to see if we need to show a notification for this report
.then(() => Ion.get(IONKEYS.SESSION, 'email'))
.then((email) => {
currentUserEmail = email;
return Ion.get(IONKEYS.CURRENT_URL);
})
.then((currentUrl) => {
// If this comment is from the current user we don't want to parrot whatever they wrote back to them.
if (reportAction.actorEmail === currentUserEmail) {
return;
}

const currentReportID = Number(lodashGet(currentUrl.split('/'), [1], 0));

// If we are currently viewing this report do not show a notification.
if (reportID === currentReportID) {
return;
}

Notification.showCommentNotification({
reportAction,
onClick: () => {
// Navigate to this report onClick
Ion.set(IONKEYS.APP_REDIRECT_TO, `/${reportID}`);
}
});
});
}

Expand Down