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

View File

@ -42,7 +42,7 @@ type Original struct {
} }
func (c *Conn) Originals(oCh chan<- Original, errc chan<- error, donec <-chan struct{}) { func (c *Conn) Originals(oCh chan<- Original, errc chan<- error, donec <-chan struct{}) {
query := ` const query = `
SELECT SELECT
uid, uid,
hash, hash,
@ -224,7 +224,7 @@ VALUES
return nil return nil
} }
var MagicAllLabel = "[all]" const MagicAllLabel = "[all]"
type Paginator struct { type Paginator struct {
Label string Label string
@ -232,11 +232,84 @@ type Paginator struct {
Count int 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. // 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 { if p.Label == MagicAllLabel {
// Paginate all messages. // Paginate all messages.
} }
// Else, filter by label. // 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 ( import (
"database/sql" "database/sql"
"encoding/json"
"flag" "flag"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -53,7 +54,17 @@ func (h *handler) LabelHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
glog.Infoln("Listing:", p) 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) glog.Errorln("LabelHandler:", err)
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return

View File

@ -2,11 +2,23 @@
* -- BASE STYLES -- * -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few. * Most of these are inherited from Base, but I want to change a few.
*/ */
body { html {
color: #333; 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 { a {
text-decoration: none; text-decoration: none;
@ -122,6 +134,7 @@ a {
color: rgb(75, 113, 151); color: rgb(75, 113, 151);
} }
.email-label,
.email-label-personal, .email-label-personal,
.email-label-work, .email-label-work,
.email-label-travel { .email-label-travel {
@ -131,6 +144,9 @@ a {
margin-right: 0.5em; margin-right: 0.5em;
border-radius: 3px; border-radius: 3px;
} }
.email-label {
background: #888;
}
.email-label-personal { .email-label-personal {
background: #ffc94c; background: #ffc94c;
} }
@ -188,7 +204,7 @@ a {
margin: 0; margin: 0;
font-weight: normal; font-weight: normal;
} }
.email-content-subtitle span { .email-content-subtitle span.date {
color: #999; color: #999;
} }
.email-content-controls { .email-content-controls {
@ -228,7 +244,7 @@ a {
} }
#nav { #nav {
margin-left:-500px; /* "left col (nav + list)" width */ margin-left:-500px; /* "left col (nav + list)" width */
width: 220px; width:150px;
height: 100%; height: 100%;
} }
@ -244,7 +260,7 @@ a {
} }
#list { #list {
margin-left: -280px; margin-left: -350px;
width: 100%; width: 100%;
height: 33%; height: 33%;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
@ -255,7 +271,7 @@ a {
top: 33%; top: 33%;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 220px; left: 150px;
overflow: auto; overflow: auto;
width: auto; /* so that it's not 100% */ 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 */ /* This will take up the entire height, and be a little thinner */
#list { #list {
margin-left: -280px; margin-left: -350px;
width: 280px; width:350px;
height: 100%; height: 100%;
border-right: 1px solid #ddd; 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]--> <!--<![endif]-->
</head> </head>
<body> <body>
<div id="layout" class="content pure-g"> <div id="content" class="loading"></div>
<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>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script src="/js/react.js"></script> <script src="/js/react.js"></script>
<script src="/js/JSXTransformer.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/folder.js" type="text/jsx"></script>
<script src="/js/main.js" type="text/jsx"></script>
<script src="/js/app.js" type="text/jsx"></script> <script src="/js/app.js" type="text/jsx"></script>
</body> </body>
</html> </html>

View File

@ -1,15 +1,39 @@
/** @jsx React.DOM */ /** @jsx React.DOM */
var App = React.createClass({ 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() { 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.renderComponent(<App/>, document.body);
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)
);

View File

@ -1,64 +1,47 @@
/** @jsx React.DOM */ /** @jsx React.DOM */
/* var Summary = React.createClass({
<div class="email-item email-item-selected pure-g"> handleClick: function(e) {
<div class="pure-u"> this.props.handleMessage(this.props.message);
<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"> render: function() {
<h5 class="email-name">Tilo Mitra</h5> var m = this.props.message;
<h4 class="email-subject">Hello from Toronto</h4> return (
<p class="email-desc"> <div key={m.Hash} onClick={this.handleClick}
Hey, I just wanted to check in with you from Toronto. I got here earlier today. className={m.Seen ? "email-item pure-g" : "email-item pure-g email-item-unread"}>
</p> <div className="pure-u">
</div> <h5 className="email-name">{m.From}</h5>
</div> <h4 className="email-subject">{m.Subject}</h4>
*/ <p className="email-desc">{m.Summary}</p>
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));
},
render: function() {
// TODO:
// - fill this out with data
// - make unread conditional
// - remove profile pic
// - drop message excerpt
// - trim 'Re:' from messages and group/thread them.
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> </div>
</div> </div>
); );
} }
}); });
React.renderComponent( var FolderView = React.createClass({
// TODO make '[all]' be set by clicking folders. propTypes: {
<FolderView source="/l/[all]" />, folderContent: React.PropTypes.array.isRequired,
$('#list').get(0) handleMessage: React.PropTypes.func.isRequired
); },
// Highlight types: email-item-{selected,unread} and none
render: function() {
// TODO:
// - fill this out with data
// - make unread conditional
// - 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">
{messages.map(function(m) {
return <Summary key={m.Hash} handleMessage={view.props.handleMessage} message={m}/>
})}
</div>
);
}
});

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))));
}
});