diff --git a/src/CONST.js b/src/CONST.js index 88147aa0a7260..29bf133b1eabd 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -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; diff --git a/src/lib/Notification/BrowserNotifications.js b/src/lib/Notification/BrowserNotifications.js new file mode 100644 index 0000000000000..22f3dd8f7c6ec --- /dev/null +++ b/src/lib/Notification/BrowserNotifications.js @@ -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`; +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, + }); + }, +}; diff --git a/src/lib/Notification/index.js b/src/lib/Notification/index.js new file mode 100644 index 0000000000000..356d9749b684e --- /dev/null +++ b/src/lib/Notification/index.js @@ -0,0 +1,9 @@ +import BrowserNotifications from './BrowserNotifications'; + +function showCommentNotification({reportAction, onClick}) { + BrowserNotifications.pushReportCommentNotification({reportAction, onClick}); +} + +export default { + showCommentNotification, +}; diff --git a/src/lib/Notification/index.native.js b/src/lib/Notification/index.native.js new file mode 100644 index 0000000000000..e1b798612c610 --- /dev/null +++ b/src/lib/Notification/index.native.js @@ -0,0 +1,4 @@ +// Browser Notifications are not supported on mobile so we'll just noop here. +export default { + showCommentNotification: () => {}, +}; diff --git a/src/lib/actions/Report.js b/src/lib/actions/Report.js index 309cdd4d7920e..24a93bc927770 100644 --- a/src/lib/actions/Report.js +++ b/src/lib/actions/Report.js @@ -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'; /** @@ -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 @@ -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}`); + } + }); }); }