feature(feedback): move feedback to a package, indicator for new msgs

Summary: Move all Intercom feedback code to a package. Change the appearance of the lower right question mark icon when a new intercom message is received (red, with repeating CSS bounce animation). New messages are detected by keeping the intercom window open (after the first time it's opened by the user), and listening for DOM mutations of particular classes.

Test Plan: manual

Reviewers: bengotow

Reviewed By: bengotow

Subscribers: evan

Differential Revision: https://phab.nylas.com/D2125
This commit is contained in:
Drew Regitsky 2015-10-07 13:55:54 -07:00
parent d634c75420
commit 7f0fca9c25
8 changed files with 299 additions and 117 deletions

View file

@ -0,0 +1,124 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Feedback</title>
<script src="nylas://feedback/node_modules/electron-safe-ipc/guest-bundle.js"></script>
<script>
(function() {
var query = location.search.substr(1);
var queryParams = {};
query.split("&").forEach(function(part) {
if (!part) return;
var item = part.split("=");
var key = item[0];
var val = decodeURIComponent(item[1])
queryParams[key] = val;
});
var _newMessages = false;
function setNewMessages(value) {
if(_newMessages !== value) // ensure we only send once message per state flip
sendNewMessageState(value);
_newMessages = value;
}
function sendNewMessageState(value) {
electronSafeIpc.send("fromRenderer", 'newFeedbackMessages', value);
}
//Hacky intercom-dependent constants
intercomClassWhitelist = [
'intercom-conversations-item',
'intercom-conversations-items'
];
function classInString(classString, className) {
return classString.split(" ").some(function(s){return s === className});
}
function classesInString(classString, classes) {
var split = classString.split(" ");
return classes.some(function(c){
return split.some(function(s){return s === c});
});
}
// Create a mutation observer to look for new messages
var mutationCallback = function(events, observer){
events.map(function(e){
var whitelisted = classesInString(e.target.className, intercomClassWhitelist);
var focus = document.hasFocus();
if(whitelisted && !focus) {
console.log(e.target.className, e);
setNewMessages(true);
}
});
};
var mutationOpts = {
childList: true,
subtree: true
};
var mutationObserver = new MutationObserver(mutationCallback);
// Listen for focus and set newMessages to false
window.onfocus = function(e) {
setNewMessages(false);
}
// Prevent window close
window.onbeforeunload = function(e) {
e.returnValue = false;
};
// Load the intercom widget.
var w = window;
var ic = w.Intercom;
if (typeof ic==="function") {
ic('reattach_activator');
ic('update', intercomSettings);
} else {
var d = document;
var i = function() { i.c(arguments) };
i.q = [];
i.c = function(args){ i.q.push(args) };
w.Intercom = i;
}
function l() {
var s = d.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'https://widget.intercom.io/widget/t7k2sjgy';
var x = d.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s,x);
}
if (w.attachEvent) {
w.attachEvent('onload',l);
} else {
w.addEventListener('load', l, false);
}
// Show the intercom messaging window.
// Send along some extra info per
// http://docs.intercom.io/configuring-Intercom/send-custom-user-attributes-to-intercom
window.Intercom('boot', {
app_id: "t7k2sjgy",
email: queryParams.email,
name: queryParams.name,
"accountId": queryParams.accountId,
"accountProvider": queryParams.accountProvider,
"platform": queryParams.platform,
"provider": queryParams.provider,
"organizational_unit": queryParams.organizational_unit,
"version": queryParams.version,
"product": "N1"
});
window.Intercom('show');
mutationObserver.observe(document, mutationOpts);
})();
</script>
</head>
<body>
<style>
.intercom-sheet-header-close-button, .intercom-sheet-header-minimize-button {
display:none !important;
}
</style>
</body></html>

View file

@ -0,0 +1,91 @@
{Utils,
React,
FocusedContactsStore,
AccountStore,
Actions} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
class FeedbackButton extends React.Component
@displayName: 'FeedbackButton'
constructor: (@props) ->
@state = {newMessages: false}
componentDidMount: =>
@unsubscribe = Actions.sendFeedback.listen(@_onSendFeedback)
componentWillUnmount: =>
@unsubscribe()
render: =>
<div style={position:"absolute",height:0}>
<div className={@_getClassName()} onClick={@_onSendFeedback}>?</div>
</div>
_getClassName: =>
return "btn-feedback" + if @state.newMessages then " newmsg" else ""
_onSendFeedback: =>
return if atom.inSpecMode()
BrowserWindow = require('remote').require('browser-window')
Screen = require('remote').require('screen')
path = require 'path'
qs = require 'querystring'
ipc_path = require.resolve("electron-safe-ipc/host")
ipc = require('remote').require(ipc_path)
if window.feedbackWindow?
window.feedbackWindow.show()
else
account = AccountStore.current()
params = qs.stringify({
name: account.name
email: account.emailAddress
accountId: account.id
accountProvider: account.provider
platform: process.platform
provider: account.displayProvider()
organizational_unit: account.organizationUnit
version: atom.getVersion()
})
parentBounds = atom.getCurrentWindow().getBounds()
parentScreen = Screen.getDisplayMatching(parentBounds)
width = 376
height = Math.min(550, parentBounds.height)
x = Math.min(parentScreen.workAreaSize.width - width, Math.max(0, parentBounds.x + parentBounds.width - 36 - width / 2))
y = Math.max(0, (parentBounds.y + parentBounds.height) - height - 60)
window.feedbackWindow = w = new BrowserWindow
'node-integration': false,
'web-preferences': {'web-security':false},
'x': x
'y': y
'width': width,
'height': height,
'title': 'Feedback'
# Disable window close, hide instead
w.on 'close', (event) ->
# inside the window we prevent close - here we route close to hide
event.preventDefault() # this does nothing, contrary to the docs
w.hide()
w.on 'closed', (event) ->
window.feedbackWindow = null # if the window does get closed, clear our ref to it
ipc.on "fromRenderer", (event,data) =>
if event == "newFeedbackMessages"
@setState(newMessages:data)
url = path.join __dirname, '..', 'feedback.html'
w.loadUrl("file://#{url}?#{params}")
w.show()
module.exports = FeedbackButton

View file

@ -0,0 +1,18 @@
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
FeedbackButton = require './feedback-button'
path = require.resolve("electron-safe-ipc/host")
ipc = require('remote').require(path)
module.exports =
activate: (@state) ->
ComponentRegistry.register FeedbackButton,
location: WorkspaceStore.Sheet.Global.Footer
serialize: ->
deactivate: ->
ComponentRegistry.unregister(FeedbackButton)

View file

@ -0,0 +1,13 @@
{
"name": "feedback",
"main": "./lib/main",
"version": "0.1.0",
"engines": {
"atom": "*"
},
"description": "Intercom feeedback",
"dependencies": {
"electron-safe-ipc": "^0.5"
},
"private":true
}

View file

@ -0,0 +1,52 @@
@import "ui-variables";
@import "ui-mixins";
.btn-feedback {
position: fixed;
bottom: 10px;
right: 10px;
background: linear-gradient(to bottom, @blue 0%,darken(@blue, 10%) 100%);
width:50px;
height:50px;
border-radius:25px;
display: inline-block;
font-size: 30px;
text-align: center;
line-height:50px;
color:rgba(255,255,255,0.9);
border: 1px solid darken(@blue, 20%);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
cursor: default;
}
.btn-feedback:hover {
color:rgba(255,255,255,1);
background: linear-gradient(to bottom, lighten(@blue,5%) 0%, darken(@blue, 5%) 100%);
}
.btn-feedback:active {
background: linear-gradient(to bottom, darken(@blue,20%) 0%, darken(@blue, 10%) 100%);
}
@keyframes bounce {
46% {right: 10px; animation-timing-function: ease-in;}
50% {right: 35px; animation-timing-function: ease-in;}
55% {right: 10px; width: 50px; animation-timing-function: ease-out;}
58% {right: 3px; width: 48px; animation-timing-function: ease-in;}
61% {right: 10px; width: 50px; animation-timing-function: ease-out;}
63% {right: 14px; animation-timing-function: ease-in;}
65% {right: 10px; animation-timing-function: ease-out;}
}
.btn-feedback.newmsg{
background: linear-gradient(to bottom, #F55 0%,darken(#F55, 10%) 100%);
border: 1px solid darken(#F55, 20%);
}
.btn-feedback.newmsg:not(:hover):not(:active){
animation: bounce 3s ease-in-out 0s infinite;
}
.btn-feedback.newmsg:hover {
color:rgba(255,255,255,1);
background: linear-gradient(to bottom, lighten(#F55,5%) 0%, darken(#F55, 5%) 100%);
}
.btn-feedback.newmsg:active {
background: linear-gradient(to bottom, darken(#F55,20%) 0%, darken(#F55, 10%) 100%);
}

View file

@ -40,7 +40,6 @@ AnalyticsStore = Reflux.createStore
init: ->
@analytics = Mixpanel.init("9a2137b80c098b3d594e39b776ebe085")
@listenTo Actions.sendFeedback, @_onSendFeedback
@listenTo AccountStore, => @identify()
@identify()
@ -92,45 +91,3 @@ AnalyticsStore = Reflux.createStore
@analytics.people.set_once(account.id, {
"First Seen": (new Date()).toISOString()
})
_onSendFeedback: ->
return if atom.inSpecMode()
{AccountStore} = require 'nylas-exports'
BrowserWindow = require('remote').require('browser-window')
Screen = require('remote').require('screen')
path = require 'path'
account = AccountStore.current()
params = qs.stringify({
name: account.name
email: account.emailAddress
accountId: account.id
accountProvider: account.provider
platform: process.platform
provider: account.displayProvider()
organizational_unit: account.organizationUnit
version: atom.getVersion()
})
parentBounds = atom.getCurrentWindow().getBounds()
parentScreen = Screen.getDisplayMatching(parentBounds)
width = 376
height = Math.min(550, parentBounds.height)
x = Math.min(parentScreen.workAreaSize.width - width, Math.max(0, parentBounds.x + parentBounds.width - 36 - width / 2))
y = Math.max(0, (parentBounds.y + parentBounds.height) - height - 60)
w = new BrowserWindow
'node-integration': false,
'web-preferences': {'web-security':false},
'x': x
'y': y
'width': width,
'height': height,
'title': 'Feedback'
{resourcePath} = atom.getLoadSettings()
url = path.join(resourcePath, 'static', 'feedback.html')
w.loadUrl("file://#{url}?#{params}")
w.show()

View file

@ -34,10 +34,6 @@ class SheetContainer extends React.Component
sheetElements = @_sheetElements()
feedbackElement = null
if atom.isMainWindow()
feedbackElement = <div className="btn-feedback" onClick={Actions.sendFeedback}>?</div>
<Flexbox direction="column" className="layout-mode-#{@state.mode}">
{@_toolbarContainerElement()}
@ -60,7 +56,7 @@ class SheetContainer extends React.Component
<InjectedComponentSet matching={locations: [topSheet.Footer, WorkspaceStore.Sheet.Global.Footer]}
direction="column"
id={topSheet.id}/>
{feedbackElement}
</div>
</Flexbox>

View file

@ -1,69 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Feedback</title>
<script>
(function() {
var query = location.search.substr(1);
var queryParams = {};
query.split("&").forEach(function(part) {
if (!part) return;
var item = part.split("=");
var key = item[0];
var val = decodeURIComponent(item[1])
queryParams[key] = val;
});
// Load the intercom widget.
var w = window;
var ic = w.Intercom;
if (typeof ic==="function") {
ic('reattach_activator');
ic('update', intercomSettings);
} else {
var d = document;
var i = function() { i.c(arguments) };
i.q = [];
i.c = function(args){ i.q.push(args) };
w.Intercom = i;
}
function l() {
var s = d.createElement('script');
s.type = 'text/javascript';
s.async = true;
s.src = 'https://widget.intercom.io/widget/t7k2sjgy';
var x = d.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s,x);
}
if (w.attachEvent) {
w.attachEvent('onload',l);
} else {
w.addEventListener('load', l, false);
}
// Show the intercom messaging window.
// Send along some extra info per
// http://docs.intercom.io/configuring-Intercom/send-custom-user-attributes-to-intercom
window.Intercom('boot', {
app_id: "t7k2sjgy",
email: queryParams.email,
name: queryParams.name,
"accountId": queryParams.accountId,
"accountProvider": queryParams.accountProvider,
"platform": queryParams.platform,
"provider": queryParams.provider,
"organizational_unit": queryParams.organizational_unit,
"version": queryParams.version,
"product": "N1"
});
window.Intercom('show');
})();
</script>
</head>
<body>
<style>
.intercom-sheet-header-close-button, .intercom-sheet-header-minimize-button {
display:none !important;
}
</style>
</body></html>