mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-02-04 14:30:57 +08:00
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:
parent
d634c75420
commit
7f0fca9c25
8 changed files with 299 additions and 117 deletions
124
internal_packages/feedback/feedback.html
Normal file
124
internal_packages/feedback/feedback.html
Normal 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>
|
91
internal_packages/feedback/lib/feedback-button.cjsx
Normal file
91
internal_packages/feedback/lib/feedback-button.cjsx
Normal 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
|
18
internal_packages/feedback/lib/main.cjsx
Normal file
18
internal_packages/feedback/lib/main.cjsx
Normal 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)
|
13
internal_packages/feedback/package.json
Normal file
13
internal_packages/feedback/package.json
Normal 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
|
||||
}
|
52
internal_packages/feedback/stylesheets/main.less
Normal file
52
internal_packages/feedback/stylesheets/main.less
Normal 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%);
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue