feat(print): Add functionality to print currently focused thread

Summary:
- Adds button inside the message list to print the thread
- Adds cmdctrl-p binding to print thread
- Adds new action and new internal_package to listen to this action.
- Creates a standalone browser window with current thread html, and removes all
collapsed messsages from the print view

Test Plan: - Manual

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2310
This commit is contained in:
Juan Tejada 2015-12-03 11:52:51 -08:00
parent d5bf5e47b7
commit 931a93af4e
14 changed files with 281 additions and 3 deletions

View file

@ -92,6 +92,7 @@ class MessageList extends React.Component
'application:reply': => @_createReplyOrUpdateExistingDraft('reply')
'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all')
'application:forward': => @_onForward()
'application:print-thread': => @_onPrintThread()
'core:messages-page-up': => @_onScrollByPage(-1)
'core:messages-page-down': => @_onScrollByPage(1)
@ -230,7 +231,10 @@ class MessageList extends React.Component
"Collapse All"
<div className="message-icons-wrap">
<div onClick={@_onToggleAllMessagesExpanded}>
<RetinaImg name="expand.png" fallback="expand.png" title={expandTitle}/>
<RetinaImg name="expand.png" fallback="expand.png" title={expandTitle} mode={RetinaImg.Mode.ContentPreserve}/>
</div>
<div onClick={@_onPrintThread}>
<RetinaImg name="print.png" fallback="print.png" title="Print Thread" mode={RetinaImg.Mode.ContentPreserve}/>
</div>
</div>
@ -265,6 +269,10 @@ class MessageList extends React.Component
_onToggleAllMessagesExpanded: ->
Actions.toggleAllMessagesExpanded()
_onPrintThread: =>
node = React.findDOMNode(@)
Actions.printThread(@state.currentThread, node.innerHTML)
_onRemoveLabel: (label) =>
task = new ChangeLabelsTask(thread: @state.currentThread, labelsToRemove: [label])
Actions.queueTask(task)

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,14 @@
import Printer from './printer';
let printer = null;
export function activate() {
printer = new Printer();
}
export function deactivate() {
if (printer) printer.deactivate();
}
export function serialize() {
}

View file

@ -0,0 +1,68 @@
import path from 'path';
import fs from 'fs';
import {remote} from 'electron';
const {app, BrowserWindow} = remote;
export default class PrintWindow {
constructor({subject, account, participants, styleTags, htmlContent, printMessages}) {
// This script will create the print prompt when loaded. We can also call
// print directly from this process, but inside print.js we can make sure to
// call window.print() after we've cleaned up the dom for printing
const scriptPath = path.join(__dirname, '..', 'static', 'print.js');
const stylesPath = path.join(__dirname, '..', 'static', 'print-styles.css');
const imgPath = path.join(__dirname, '..', 'assets', 'nylas-print-logo.png');
const participantsHtml = participants.map((part) => {
return (`<li class="participant"><span>${part.name} &lt;${part.email}&gt;</span></li>`);
}).join('');
const content = (`
<html>
<head>
<meta charset="utf-8">
${styleTags}
<link rel="stylesheet" type="text/css" href="${stylesPath}">
</head>
<body>
<div id="print-header">
<div class="logo-wrapper">
<img src="${imgPath}" alt="nylas-logo"/>
<span class="account">${account.name} &lt;${account.email}&gt;</span>
</div>
<h1>${subject}</h1>
<div class="participants">
<ul>
${participantsHtml}
</ul>
</div>
</div>
${htmlContent}
<script type="text/javascript">
window.printMessages = ${printMessages}
</script>
<script type="text/javascript" src="${scriptPath}"></script>
</body>
</html>
`);
this.tmpFile = path.join(app.getPath('temp'), 'print.html');
this.browserWin = new BrowserWindow({
width: 800,
height: 600,
title: `Print - ${subject}`,
webPreferences: {
nodeIntegration: false,
},
});
fs.writeFileSync(this.tmpFile, content);
}
/**
* Load our temp html file. Once the file is loaded it will run print.js, and
* that script will pop out the print dialog.
*/
load() {
this.browserWin.loadURL(`file://${this.tmpFile}`);
}
}

View file

@ -0,0 +1,42 @@
import {AccountStore, Actions} from 'nylas-exports';
import PrintWindow from './print-window';
class Printer {
constructor() {
this.unsub = Actions.printThread.listen(this._printThread);
}
_printThread(thread, htmlContent) {
if (!thread) throw new Error('Printing: No thread active!');
// Get the <nylas-styles> tag present in the document
const styleTag = document.getElementsByTagName('nylas-styles')[0];
// These iframes should correspond to the message iframes when a thread is
// focused
const iframes = document.getElementsByTagName('iframe');
// Grab the html inside the iframes
const messagesHtml = [].slice.call(iframes).map((iframe)=> {
return iframe.contentDocument.documentElement.innerHTML;
});
const win = new PrintWindow({
subject: thread.subject,
account: {
name: AccountStore.current().name,
email: AccountStore.current().emailAddress,
},
participants: thread.participants,
styleTags: styleTag.innerHTML,
htmlContent,
printMessages: JSON.stringify(messagesHtml),
});
win.load();
}
deactivate() {
this.unsub();
}
}
export default Printer;

View file

@ -0,0 +1,11 @@
{
"name": "print",
"version": "0.1.0",
"main": "./lib/main",
"description": "Print",
"license": "GPLv3",
"private": true,
"engines": {
"nylas": "*"
}
}

View file

@ -0,0 +1,74 @@
body {
overflow: auto !important;
}
#print-header {
padding: 15px 20px 0 20px;
}
#print-header img {
zoom: 0.5;
}
#print-header .logo-wrapper {
display: flex;
align-items: center;
font-family: "Nylas-Pro", "Helvetica", "Lucidia Grande", sans-serif !important;
}
#print-header h1 {
font-size: 1.5em !important;
font-family: "Nylas-Pro", "Helvetica", "Lucidia Grande", sans-serif !important;
}
#print-header .account {
margin-left: auto;
font-size: 0.8em !important;
}
#print-header .participant {
font-size: 0.7em;
font-family: "Nylas-Pro", "Helvetica", "Lucidia Grande", sans-serif !important;
}
/* Elements to hide */
.message-subject-wrap {
display: none !important;
}
.minified-bundle,.headers,.scrollbar-track,.message-icons-wrap,.header-toggle-control {
display: none !important;
}
.message-actions-wrap {
display: none;
}
.collapsed.message-item-wrap,.draft.message-item-wrap {
display: none !important;
}
.message-item-area>div {
display: none !important;
}
.quoted-text-control, .footer-reply-area-wrap {
display: none;
}
@media only print {
body,#message-list,.message-item-wrap,.message-item-white-wrap,
.message-item-area,.inbox-html-wrapper {
display: block !important;
width: auto !important;
height: auto !important;
overflow: visible !important;
}
#message-list {
min-height: initial;
}
#print-header {
padding: 0;
}
#print-header .account {
font-size: 0.7em;
}
.message-item-wrap {
display: block;
}
.message-item-area>span {
page-break-before: avoid;
}
.message-item-area>header {
page-break-after: avoid;
}
}

View file

@ -0,0 +1,41 @@
(function() {
function rebuildMessages(messageNodes, messages) {
// Simply insert the message html inside the appropriate node
for (var idx = 0; idx < messageNodes.length; idx++) {
var msgNode = messageNodes[idx];
var msgHtml = messages[idx];
msgNode.innerHTML = msgHtml;
}
}
function removeClassFromNodes(nodeList, className) {
for (var idx = 0; idx < nodeList.length; idx++) {
var node = nodeList[idx];
var re = new RegExp('\\b' + className + '\\b', 'g');
node.className = node.className.replace(re, '');
}
}
function removeScrollClasses() {
var scrollRegions = document.querySelectorAll('.scroll-region');
var scrollContents = document.querySelectorAll('.scroll-region-content');
var scrollContentInners = document.querySelectorAll('.scroll-region-content-inner');
removeClassFromNodes(scrollRegions, 'scroll-region');
removeClassFromNodes(scrollContents, 'scroll-region-content');
removeClassFromNodes(scrollContentInners, 'scroll-region-content-inner');
}
function print() {
window.print();
// Close this print window after selecting to print
// This is really hackish but appears to be the only working solution
setTimeout(window.close, 500);
}
var messageNodes = document.querySelectorAll('.message-item-area>span');
removeScrollClasses();
rebuildMessages(messageNodes, window.printMessages);
// Give it a few ms before poppint out the print dialog
setTimeout(print, 50);
})();

View file

@ -41,6 +41,7 @@
### Core application commands. ###
'cmdctrl-q' : 'application:quit'
'cmdctrl-w' : 'window:close'
'cmdctrl-p' : 'application:print-thread'
### Universal N1 commands. ###
'enter' : 'core:focus-item'

View file

@ -28,6 +28,8 @@
{ label: 'New Message', command: 'application:new-message' }
{ type: 'separator' }
{ label: 'Close Window', command: 'window:close' }
{ type: 'separator' }
{ label: 'Print Current Thread', command: 'application:print-thread' }
]
}

View file

@ -4,9 +4,11 @@
submenu: [
{ label: '&New Message', command: 'application:new-message' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ label: 'Add Account...', command: 'application:add-account' }
{ label: 'Clos&e Window', command: 'window:close' }
{ type: 'separator' }
{ label: 'Print Current Thread', command: 'application:print-thread' }
{ type: 'separator' }
{ label: 'Quit', command: 'application:quit' }
]
}
@ -21,6 +23,8 @@
{ label: 'C&opy', command: 'core:copy' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Select &All', command: 'core:select-all' }
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
]
}

View file

@ -55,6 +55,8 @@
{ type: 'separator' }
{ label: 'Preferences', command: 'application:open-preferences' }
{ type: 'separator' }
{ label: 'Print Current Thread', command: 'application:print-thread' }
{ type: 'separator' }
{ label: 'E&xit', command: 'application:quit' }
]

View file

@ -231,12 +231,23 @@ class Actions
*Scope: Window*
```
message = <Message>
Actions.toggleAllMessagesExpanded()
```
###
@toggleAllMessagesExpanded: ActionScopeWindow
###
Public: Print the currently selected thread.
*Scope: Window*
```
thread = <Thread>
Actions.printThread(thread)
```
###
@printThread: ActionScopeWindow
###
Public: Create a new reply to the provided threadId and messageId and populate
it with the body provided.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB