First version to show (raw) email bodies.

Made xwebmail work with new handlers package.
Pulls important headers from the database and provides extremely basic folder
view on webpage.
Reverted layout customizations returning folder view to original wider width.
JS App now handles all the rendering, index.html only contains placeholder
with background to indicate loading.
This commit is contained in:
Bill Thiede 2014-04-16 22:23:45 -07:00
parent 554842a3fb
commit 9bcfb1bf21
11 changed files with 268 additions and 282 deletions

View File

@ -5,9 +5,9 @@ import (
"net/http"
"github.com/golang/glog"
"github.com/gorilla/mux"
"xinu.tv/email/db"
"xinu.tv/email/handlers"
)
var addr = flag.String("addr", ":8080", "address:port to listen on")
@ -21,10 +21,6 @@ func main() {
glog.Fatal(err)
}
h := &handler{c: c}
r := mux.NewRouter()
r.HandleFunc("/raw/{hash}", h.OriginalHandler)
r.HandleFunc("/l/{label}", h.LabelHandler)
glog.Fatal(http.ListenAndServe(*addr, r))
h := handlers.Handlers(c)
glog.Fatal(http.ListenAndServe(*addr, h))
}

View File

@ -42,7 +42,7 @@ type Original struct {
}
func (c *Conn) Originals(oCh chan<- Original, errc chan<- error, donec <-chan struct{}) {
query := `
const query = `
SELECT
uid,
hash,
@ -224,7 +224,7 @@ VALUES
return nil
}
var MagicAllLabel = "[all]"
const MagicAllLabel = "[all]"
type Paginator struct {
Label string
@ -232,11 +232,84 @@ type Paginator struct {
Count int
}
type Address mail.Address
type MessageHeader struct {
Hash string
From *Address
To []*Address
Cc []*Address
Subject string
Date time.Time
Seen bool
// TODO
Summary, ListId string
}
// Index returns ranges of metadata for messages as defined by paginator.
func (c *Conn) Index(p *Paginator) error {
func (c *Conn) Index(p *Paginator) ([]*MessageHeader, error) {
const query = `
SELECT
hash, name, value
FROM
search_header
WHERE
name IN ('Date', 'Subject', 'To', 'From', 'Cc')
ORDER BY
hash
LIMIT
100
-- TODO add offset based on Paginator
;
`
var res []*MessageHeader
if p.Label == MagicAllLabel {
// Paginate all messages.
}
// Else, filter by label.
return nil
rows, err := c.Query(query)
if err != nil {
return res, err
}
var mh *MessageHeader
for rows.Next() {
var hash, name, value string
if err := rows.Scan(&hash, &name, &value); err != nil {
return res, err
}
if mh == nil {
// First Row
mh = new(MessageHeader)
mh.Hash = hash
}
if hash != mh.Hash {
res = append(res, mh)
mh = new(MessageHeader)
mh.Hash = hash
}
switch name {
case "Date":
t, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return res, err
}
mh.Date = t
case "Subject":
mh.Subject = value
case "To":
mh.To = append(mh.To, &Address{Address: value})
case "From":
mh.From = &Address{Address: value}
case "Cc":
mh.Cc = append(mh.Cc, &Address{Address: value})
}
}
// Intentionally don't handle the last MessageHeader, it is likely
// incomplete if LIMIT in the query cuts off a message midstream.
if err := rows.Err(); err != nil {
return res, err
}
return res, nil
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"database/sql"
"encoding/json"
"flag"
"net/http"
"path/filepath"
@ -53,7 +54,17 @@ func (h *handler) LabelHandler(w http.ResponseWriter, r *http.Request) {
return
}
glog.Infoln("Listing:", p)
if err := h.c.Index(p); err != nil {
res, err := h.c.Index(p)
if err != nil {
glog.Errorln("LabelHandler:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
err = enc.Encode(res)
if err != nil {
glog.Errorln("LabelHandler:", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -2,11 +2,23 @@
* -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few.
*/
body {
color: #333;
html {
height: 100%;
}
#content.loading {
background-image: url('/img/email-icon.jpg');
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
height: 100%;
width: 100%;
}
body {
color: #333;
height: 100%;
}
a {
text-decoration: none;
@ -122,6 +134,7 @@ a {
color: rgb(75, 113, 151);
}
.email-label,
.email-label-personal,
.email-label-work,
.email-label-travel {
@ -131,6 +144,9 @@ a {
margin-right: 0.5em;
border-radius: 3px;
}
.email-label {
background: #888;
}
.email-label-personal {
background: #ffc94c;
}
@ -188,7 +204,7 @@ a {
margin: 0;
font-weight: normal;
}
.email-content-subtitle span {
.email-content-subtitle span.date {
color: #999;
}
.email-content-controls {
@ -228,7 +244,7 @@ a {
}
#nav {
margin-left:-500px; /* "left col (nav + list)" width */
width: 220px;
width:150px;
height: 100%;
}
@ -244,7 +260,7 @@ a {
}
#list {
margin-left: -280px;
margin-left: -350px;
width: 100%;
height: 33%;
border-bottom: 1px solid #ddd;
@ -255,7 +271,7 @@ a {
top: 33%;
right: 0;
bottom: 0;
left: 220px;
left: 150px;
overflow: auto;
width: auto; /* so that it's not 100% */
}
@ -271,8 +287,8 @@ a {
/* This will take up the entire height, and be a little thinner */
#list {
margin-left: -280px;
width: 280px;
margin-left: -350px;
width:350px;
height: 100%;
border-right: 1px solid #ddd;
}

BIN
static/img/email-icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

View File

@ -14,166 +14,13 @@
<!--<![endif]-->
</head>
<body>
<div id="layout" class="content pure-g">
<div id="nav" class="pure-u">
<a href="#" class="nav-menu-button">Menu</a>
<div class="nav-inner">
<div id="unread-list" class="pure-menu pure-menu-open">
<ul>
<li><a href="#">No unread mail.</a></li>
<li class="pure-menu-heading">Labels</li>
<li><a href="#"><span class="email-label-personal"></span>Personal</a></li>
<li><a href="#"><span class="email-label-work"></span>Work</a></li>
<li><a href="#"><span class="email-label-travel"></span>Travel</a></li>
</ul>
</div>
</div>
</div>
<div id="list" class="pure-u-1">
<div class="email-item email-item-selected pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Tilo Mitra&#x27;s avatar" height="64" width="64" src="img/common/tilo-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Tilo Mitra</h5>
<h4 class="email-subject">Hello from Toronto</h4>
<p class="email-desc">
Hey, I just wanted to check in with you from Toronto. I got here earlier today.
</p>
</div>
</div>
<div class="email-item email-item-unread pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Eric Ferraiuolo&#x27;s avatar" height="64" width="64" src="img/common/ericf-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Eric Ferraiuolo</h5>
<h4 class="email-subject">Re: Pull Requests</h4>
<p class="email-desc">
Hey, I had some feedback for pull request #51. We should center the menu so it looks better on mobile.
</p>
</div>
</div>
<div class="email-item email-item-unread pure-g">
<div class="pure-u">
<img class="email-avatar" alt="YUI&#x27;s avatar" height="64" width="64" src="img/common/yui-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">YUI Library</h5>
<h4 class="email-subject">You have 5 bugs assigned to you</h4>
<p class="email-desc">
Duis aute irure dolor in reprehenderit in voluptate velit essecillum dolore eu fugiat nulla.
</p>
</div>
</div>
<div class="email-item pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Reid Burke&#x27;s avatar" height="64" width="64" src="img/common/reid-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Reid Burke</h5>
<h4 class="email-subject">Re: Design Language</h4>
<p class="email-desc">
Excepteur sint occaecat cupidatat non proident, sunt in culpa.
</p>
</div>
</div>
<div class="email-item pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Andrew Wooldridge&#x27;s avatar" height="64" width="64" src="img/common/andrew-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Andrew Wooldridge</h5>
<h4 class="email-subject">YUI Blog Updates?</h4>
<p class="email-desc">
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip.
</p>
</div>
</div>
<div class="email-item pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Yahoo! Finance&#x27;s Avatar" height="64" width="64" src="img/common/yfinance-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Yahoo! Finance</h5>
<h4 class="email-subject">How to protect your finances from winter storms</h4>
<p class="email-desc">
Mauris tempor mi vitae sem aliquet pharetra. Fusce in dui purus, nec malesuada mauris.
</p>
</div>
</div>
<div class="email-item pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Yahoo! News&#x27; avatar" height="64" width="64" src="img/common/ynews-avatar.png">
</div>
<div class="pure-u-3-4">
<h5 class="email-name">Yahoo! News</h5>
<h4 class="email-subject">Summary for April 3rd, 2012</h4>
<p class="email-desc">
We found 10 news articles that you may like.
</p>
</div>
</div>
</div>
<div id="main" class="pure-u-1">
<div class="email-content">
<div class="email-content-header pure-g">
<div class="pure-u-1-2">
<h1 class="email-content-title">Hello from Toronto</h1>
<p class="email-content-subtitle">
From <a>Tilo Mitra</a> at <span>3:56pm, April 3, 2012</span>
</p>
</div>
<div class="email-content-controls pure-u-1-2">
<button class="secondary-button pure-button">Reply</button>
<button class="secondary-button pure-button">Forward</button>
<button class="secondary-button pure-button">Move to</button>
</div>
</div>
<div class="email-content-body">
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
</p>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit essecillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Aliquam ac feugiat dolor. Proin mattis massa sit amet enim iaculis tincidunt. Mauris tempor mi vitae sem aliquet pharetra. Fusce in dui purus, nec malesuada mauris. Curabitur ornare arcu quis mi blandit laoreet. Vivamus imperdiet fermentum mauris, ac posuere urna tempor at. Duis pellentesque justo ac sapien aliquet egestas. Morbi enim mi, porta eget ullamcorper at, pharetra id lorem.
</p>
<p>
Donec sagittis dolor ut quam pharetra pretium varius in nibh. Suspendisse potenti. Donec imperdiet, velit vel adipiscing bibendum, leo eros tristique augue, eu rutrum lacus sapien vel quam. Nam orci arcu, luctus quis vestibulum ut, ullamcorper ut enim. Morbi semper erat quis orci aliquet condimentum. Nam interdum mauris sed massa dignissim rhoncus.
</p>
<p>
Regards,<br>
Tilo
</p>
</div>
</div>
</div>
</div>
<div id="content" class="loading"></div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="/js/react.js"></script>
<script src="/js/JSXTransformer.js"></script>
<script src="/js/unread.js" type="text/jsx"></script>
<script src="/js/nav.js" type="text/jsx"></script>
<script src="/js/folder.js" type="text/jsx"></script>
<script src="/js/main.js" type="text/jsx"></script>
<script src="/js/app.js" type="text/jsx"></script>
</body>
</html>

View File

@ -1,15 +1,39 @@
/** @jsx React.DOM */
var App = React.createClass({
getInitialState: function() {
return {
// TODO make this change by clicking on folder view.
source: "/l/[all]",
folderContent: [],
currentMessage: null
};
},
componentWillMount: function() {
$.get(this.state.source, function(result) {
this.setState({folderContent: result});
this.setMessage(result[0]);
}.bind(this));
},
setMessage: function(msg) {
this.setState({currentMessage: msg});
},
render: function() {
return React.DOM.div({className: ''});
if (this.state.currentMessage == null) {
return (<div id="content" className="loading">Loading...</div>);
}
return (
<div id="layout" className="content pure-g">
<NavView source="//mail.z.xinu.tv/unread"/>
{/* TODO make '[all]' be set by clicking folders. */}
<FolderView handleMessage={this.setMessage} folderContent={this.state.folderContent}/>
<MainView message={this.state.currentMessage}/>
</div>
);
}
});
React.renderComponent(
React.DOM.div({id: 'layout', className: 'content pure-g'},
<UnreadCount source="//mail.z.xinu.tv/unread" />,
// TODO make '[all]' be set by clicking folders.
<FolderView source="/l/[all]" />),
$('body').get(0)
);
React.renderComponent(<App/>, document.body);

View File

@ -1,35 +1,32 @@
/** @jsx React.DOM */
/*
<div class="email-item email-item-selected pure-g">
<div class="pure-u">
<img class="email-avatar" alt="Tilo Mitra&#x27;s avatar" height="64" width="64" src="img/common/tilo-avatar.png">
</div>
var Summary = React.createClass({
handleClick: function(e) {
this.props.handleMessage(this.props.message);
},
<div class="pure-u-3-4">
<h5 class="email-name">Tilo Mitra</h5>
<h4 class="email-subject">Hello from Toronto</h4>
<p class="email-desc">
Hey, I just wanted to check in with you from Toronto. I got here earlier today.
</p>
render: function() {
var m = this.props.message;
return (
<div key={m.Hash} onClick={this.handleClick}
className={m.Seen ? "email-item pure-g" : "email-item pure-g email-item-unread"}>
<div className="pure-u">
<h5 className="email-name">{m.From}</h5>
<h4 className="email-subject">{m.Subject}</h4>
<p className="email-desc">{m.Summary}</p>
</div>
</div>
*/
);
}
});
var FolderView = React.createClass({
getInitialState: function() {
return {
list: {}
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) {
console.log('FolderView $.get', result);
this.setState({list: result});
}.bind(this));
propTypes: {
folderContent: React.PropTypes.array.isRequired,
handleMessage: React.PropTypes.func.isRequired
},
// Highlight types: email-item-{selected,unread} and none
render: function() {
// TODO:
// - fill this out with data
@ -37,28 +34,14 @@ var FolderView = React.createClass({
// - remove profile pic
// - drop message excerpt
// - trim 'Re:' from messages and group/thread them.
var messages = this.props.folderContent,
view = this;
return (
<div id="list" className="pure-u-1">
<div className="email-item email-item-selected pure-g">
<div className="pure-u">
<img className="email-avatar" alt="Tilo Mitra&#x27;s avatar" height="64" width="64" src="img/common/tilo-avatar.png" />
</div>
<div className="pure-u-3-4">
<h5 className="email-name">Tilo Mitra</h5>
<h4 className="email-subject">Hello from Toronto</h4>
<p className="email-desc">
Hey, I just wanted to check in with you from Toronto. I got here earlier today.
</p>
</div>
</div>
{messages.map(function(m) {
return <Summary key={m.Hash} handleMessage={view.props.handleMessage} message={m}/>
})}
</div>
);
);
}
});
React.renderComponent(
// TODO make '[all]' be set by clicking folders.
<FolderView source="/l/[all]" />,
$('#list').get(0)
);

42
static/js/main.js Normal file
View File

@ -0,0 +1,42 @@
/** @jsx React.DOM */
var MainView = React.createClass({
getInitialState: function() {
return {
body: ""
};
},
render: function() {
$.get("/raw/" + this.props.message.Hash, function(data) {
var body = data.substring(data.indexOf('\n\n')+2);
this.setState({body: body});
}.bind(this));
// TODO:
// - trim 'Re:' from messages and group/thread them.
var m = this.props.message;
return (
<div id="main" className="pure-u-1">
<div className="email-content">
<div className="email-content-header pure-g">
<div className="pure-u-1">
<h1 className="email-content-title">{m.Subject}</h1>
<p className="email-content-subtitle">
From <a>{m.From}</a> at <span className="date">{m.Date.toLocaleString()}</span>
</p>
</div>
<div className="email-content-controls pure-u-1">
<button className="secondary-button pure-button">Reply</button>
<button className="secondary-button pure-button">Forward</button>
<a href={"/raw/"+m.Hash} className="secondary-button pure-button">Original</a>
</div>
</div>
<div className="email-content-body"><pre>{this.state.body}</pre></div>
</div>
</div>
);
}
});

36
static/js/nav.js Normal file
View File

@ -0,0 +1,36 @@
/** @jsx React.DOM */
var NavView = React.createClass({
getInitialState: function() {
return {
unreadCount: {}
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) {
console.log('NavView $.get', result);
this.setState({unreadCount: result});
}.bind(this));
},
render: function() {
var keys = Object.keys(this.state.unreadCount).sort();
var lis = [];
for (var i in keys) {
var key = keys[i];
var value = this.state.unreadCount[key];
lis.push(<li key={key}><a href="#">{key}</a></li>);
}
return (
<div id="nav" className="pure-u">
<div className="nav-inner">
<button className="primary-button pure-button">Compose</button>
<div id="unread-list" className="pure-menu pure-menu-open">
<ul>{lis}</ul>
</div>
</div>
</div>
);
}
});

View File

@ -1,42 +0,0 @@
/** @jsx React.DOM */
// TODO rename this nav.
var UnreadCount = React.createClass({
getInitialState: function() {
return {
unreadCount: {}
};
},
componentDidMount: function() {
$.get(this.props.source, function(result) {
console.log('UnreadCount $.get', result);
this.setState({unreadCount: result});
}.bind(this));
},
render: function() {
var keys = Object.keys(this.state.unreadCount).sort();
console.log('unread keys', keys);
var lis = [];
for (var i in keys) {
var key = keys[i];
var value = this.state.unreadCount[key];
var dom = React.DOM.li({key: key}, React.DOM.a({href: '#'},
React.DOM.span({className: 'email-count'}, '(', value, ')'),
' ', key
));
lis.push(dom);
}
console.log('Found', lis);
// TODO rewrite as JSX template? How to handle repreating fields?
return React.DOM.div({id: 'nav', className: 'pure-u'},
React.DOM.div({className: 'nav-inner'},
React.DOM.div({
id: 'unread-list',
className: 'pure-menu pure-menu-open'
}, React.DOM.ul({},
lis))));
}
});