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:
parent
554842a3fb
commit
9bcfb1bf21
@ -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))
|
||||
}
|
||||
|
||||
81
db/util.go
81
db/util.go
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
BIN
static/img/email-icon.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
@ -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'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'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'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'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'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'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' 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>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,64 +1,47 @@
|
||||
/** @jsx React.DOM */
|
||||
|
||||
/*
|
||||
<div class="email-item email-item-selected pure-g">
|
||||
<div class="pure-u">
|
||||
<img class="email-avatar" alt="Tilo Mitra'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>
|
||||
</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));
|
||||
},
|
||||
|
||||
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'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>
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
React.renderComponent(
|
||||
// TODO make '[all]' be set by clicking folders.
|
||||
<FolderView source="/l/[all]" />,
|
||||
$('#list').get(0)
|
||||
);
|
||||
var FolderView = React.createClass({
|
||||
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
|
||||
// - 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
42
static/js/main.js
Normal 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
36
static/js/nav.js
Normal 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>
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -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))));
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user