Merge pull request #356 from writefreely/draft-list-paging
Draft list paging
This commit is contained in:
commit
73450a50e3
19
account.go
19
account.go
|
@ -16,6 +16,7 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -691,6 +692,22 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
|
||||||
return ErrBadRequestedType
|
return ErrBadRequestedType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAnonPosts := r.FormValue("anonymous") == "1"
|
||||||
|
if isAnonPosts {
|
||||||
|
pageStr := r.FormValue("page")
|
||||||
|
pg, err := strconv.Atoi(pageStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Error parsing page parameter '%s': %s", pageStr, err)
|
||||||
|
pg = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := app.db.GetAnonymousPosts(u, pg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return impart.WriteSuccess(w, p, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
p := GetPostsCache(u.ID)
|
p := GetPostsCache(u.ID)
|
||||||
if p == nil {
|
if p == nil {
|
||||||
|
@ -731,7 +748,7 @@ func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Requ
|
||||||
}
|
}
|
||||||
|
|
||||||
func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
|
||||||
p, err := app.db.GetAnonymousPosts(u)
|
p, err := app.db.GetAnonymousPosts(u, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("unable to fetch anon posts: %v", err)
|
log.Error("unable to fetch anon posts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
17
database.go
17
database.go
|
@ -77,7 +77,7 @@ type writestore interface {
|
||||||
GetTotalCollections() (int64, error)
|
GetTotalCollections() (int64, error)
|
||||||
GetTotalPosts() (int64, error)
|
GetTotalPosts() (int64, error)
|
||||||
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
GetTopPosts(u *User, alias string) (*[]PublicPost, error)
|
||||||
GetAnonymousPosts(u *User) (*[]PublicPost, error)
|
GetAnonymousPosts(u *User, page int) (*[]PublicPost, error)
|
||||||
GetUserPosts(u *User) (*[]PublicPost, error)
|
GetUserPosts(u *User) (*[]PublicPost, error)
|
||||||
|
|
||||||
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error)
|
CreateOwnedPost(post *SubmittedPost, accessToken, collAlias, hostName string) (*PublicPost, error)
|
||||||
|
@ -1806,8 +1806,19 @@ func (db *datastore) GetTopPosts(u *User, alias string) (*[]PublicPost, error) {
|
||||||
return &posts, nil
|
return &posts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *datastore) GetAnonymousPosts(u *User) (*[]PublicPost, error) {
|
func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) {
|
||||||
rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC", u.ID)
|
pagePosts := 10
|
||||||
|
start := page*pagePosts - pagePosts
|
||||||
|
if page == 0 {
|
||||||
|
start = 0
|
||||||
|
pagePosts = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
limitStr := ""
|
||||||
|
if page > 0 {
|
||||||
|
limitStr = fmt.Sprintf(" LIMIT %d, %d", start, pagePosts)
|
||||||
|
}
|
||||||
|
rows, err := db.Query("SELECT id, view_count, title, created, updated, content FROM posts WHERE owner_id = ? AND collection_id IS NULL ORDER BY created DESC"+limitStr, u.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Failed selecting from posts: %v", err)
|
log.Error("Failed selecting from posts: %v", err)
|
||||||
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}
|
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}
|
||||||
|
|
|
@ -110,7 +110,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
|
||||||
log.Error("unable to fetch collections: %v", err)
|
log.Error("unable to fetch collections: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := app.db.GetAnonymousPosts(u)
|
posts, err := app.db.GetAnonymousPosts(u, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("unable to fetch anon posts: %v", err)
|
log.Error("unable to fetch anon posts: %v", err)
|
||||||
}
|
}
|
||||||
|
|
20
handle.go
20
handle.go
|
@ -287,6 +287,26 @@ func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
|
||||||
return h.UserAll(false, f, apiAuth)
|
return h.UserAll(false, f, apiAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserWebAPI handles endpoints that accept a user authorized either via the web (cookies) or an Authorization header.
|
||||||
|
func (h *Handler) UserWebAPI(f userHandlerFunc) http.HandlerFunc {
|
||||||
|
return h.UserAll(false, f, func(app *App, r *http.Request) (*User, error) {
|
||||||
|
// Authorize user via cookies
|
||||||
|
u := getUserSession(app, r)
|
||||||
|
if u != nil {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fall back to access token, since user isn't logged in via web
|
||||||
|
var err error
|
||||||
|
u, err = apiAuth(app, r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
|
func (h *Handler) UserAll(web bool, f userHandlerFunc, a authFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
handleFunc := func() error {
|
handleFunc := func() error {
|
||||||
|
|
|
@ -115,7 +115,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
|
||||||
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
|
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
|
||||||
apiMe := write.PathPrefix("/api/me/").Subrouter()
|
apiMe := write.PathPrefix("/api/me/").Subrouter()
|
||||||
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
|
apiMe.HandleFunc("/", handler.All(viewMeAPI)).Methods("GET")
|
||||||
apiMe.HandleFunc("/posts", handler.UserAPI(viewMyPostsAPI)).Methods("GET")
|
apiMe.HandleFunc("/posts", handler.UserWebAPI(viewMyPostsAPI)).Methods("GET")
|
||||||
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
|
apiMe.HandleFunc("/collections", handler.UserAPI(viewMyCollectionsAPI)).Methods("GET")
|
||||||
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
|
apiMe.HandleFunc("/password", handler.All(updatePassphrase)).Methods("POST")
|
||||||
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
|
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")
|
||||||
|
|
|
@ -181,9 +181,17 @@ var localPosts = function() {
|
||||||
undoDelete: UndoDelete,
|
undoDelete: UndoDelete,
|
||||||
};
|
};
|
||||||
}();
|
}();
|
||||||
var createPostEl = function(post) {
|
var movePostHTML = function(postID) {
|
||||||
|
let $tmpl = document.getElementById('move-tmpl');
|
||||||
|
if ($tmpl === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return $tmpl.innerHTML.replace(/POST_ID/g, postID);
|
||||||
|
}
|
||||||
|
var createPostEl = function(post, owned) {
|
||||||
var $post = document.createElement('div');
|
var $post = document.createElement('div');
|
||||||
var title = (post.title || post.id);
|
let p = H.createPost(post.id, "", post.body)
|
||||||
|
var title = (post.title || p.title || post.id);
|
||||||
title = title.replace(/</g, "<");
|
title = title.replace(/</g, "<");
|
||||||
$post.id = 'post-' + post.id;
|
$post.id = 'post-' + post.id;
|
||||||
$post.className = 'post';
|
$post.className = 'post';
|
||||||
|
@ -194,13 +202,22 @@ var createPostEl = function(post) {
|
||||||
posted = getFormattedDate(new Date(post.created))
|
posted = getFormattedDate(new Date(post.created))
|
||||||
}
|
}
|
||||||
var hasDraft = H.exists('draft' + post.id);
|
var hasDraft = H.exists('draft' + post.id);
|
||||||
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\')">delete</a></h4>';
|
$post.innerHTML += '<h4><date>' + posted + '</date> <a class="action" href="/pad/' + post.id + '">edit' + (hasDraft ? 'ed' : '') + '</a> <a class="delete action" href="/' + post.id + '" onclick="delPost(event, \'' + post.id + '\'' + (owned === true ? ', true' : '') + ')">delete</a> '+movePostHTML(post.id)+'</h4>';
|
||||||
|
|
||||||
if (post.error) {
|
if (post.error) {
|
||||||
$post.innerHTML += '<p class="error"><strong>Sync error:</strong> ' + post.error + ' <nav><a href="#" onclick="localPosts.dismissError(event, this)">dismiss</a> <a href="#" onclick="localPosts.deletePost(event, this, \''+post.id+'\')">remove post</a></nav></p>';
|
$post.innerHTML += '<p class="error"><strong>Sync error:</strong> ' + post.error + ' <nav><a href="#" onclick="localPosts.dismissError(event, this)">dismiss</a> <a href="#" onclick="localPosts.deletePost(event, this, \''+post.id+'\')">remove post</a></nav></p>';
|
||||||
}
|
}
|
||||||
if (post.summary) {
|
if (post.summary) {
|
||||||
|
// TODO: switch to using p.summary, after ensuring it matches summary generated on the backend.
|
||||||
$post.innerHTML += '<p>' + post.summary.replace(/</g, "<") + '</p>';
|
$post.innerHTML += '<p>' + post.summary.replace(/</g, "<") + '</p>';
|
||||||
|
} else if (post.body) {
|
||||||
|
var preview;
|
||||||
|
if (post.body.length > 140) {
|
||||||
|
preview = post.body.substr(0, 140) + '...';
|
||||||
|
} else {
|
||||||
|
preview = post.body;
|
||||||
|
}
|
||||||
|
$post.innerHTML += '<p>' + preview.replace(/</g, "<") + '</p>';
|
||||||
}
|
}
|
||||||
return $post;
|
return $post;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
{{define "articles"}}
|
{{define "articles"}}
|
||||||
{{template "header" .}}
|
{{template "header" .}}
|
||||||
|
<style type="text/css">
|
||||||
|
a.loading {
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
#move-tmpl {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<div class="snug content-container">
|
<div class="snug content-container">
|
||||||
|
|
||||||
|
@ -15,7 +24,7 @@
|
||||||
{{ if .AnonymousPosts }}
|
{{ if .AnonymousPosts }}
|
||||||
<p>These are your draft posts. You can share them individually (without a blog) or move them to your blog when you're ready.</p>
|
<p>These are your draft posts. You can share them individually (without a blog) or move them to your blog when you're ready.</p>
|
||||||
|
|
||||||
<div class="atoms posts">
|
<div id="anon-posts" class="atoms posts">
|
||||||
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
|
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
|
||||||
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
|
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
|
||||||
<h4>
|
<h4>
|
||||||
|
@ -39,9 +48,12 @@
|
||||||
</h4>
|
</h4>
|
||||||
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{end}}
|
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{end}}
|
||||||
</div>{{end}}
|
</div>{{end}}
|
||||||
</div>{{ else }}<div id="no-posts-published">
|
</div>
|
||||||
|
{{if eq (len .AnonymousPosts) 10}}<p id="load-more-p"><a href="#load">Load more...</a></p>{{end}}
|
||||||
|
{{ else }}<div id="no-posts-published">
|
||||||
<p>Your anonymous and draft posts will show up here once you've published some. You'll be able to share them individually (without a blog) or move them to a blog when you're ready.</p>
|
<p>Your anonymous and draft posts will show up here once you've published some. You'll be able to share them individually (without a blog) or move them to a blog when you're ready.</p>
|
||||||
{{if not .SingleUser}}<p>Alternatively, see your blogs and their posts on your <a href="/me/c/">Blogs</a> page.</p>{{end}}
|
{{if not .SingleUser}}<p>Alternatively, see your blogs and their posts on your <a href="/me/c/">Blogs</a> page.</p>{{end}}
|
||||||
|
|
||||||
<p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }}
|
<p class="text-cta"><a href="{{if .SingleUser}}/me/new{{else}}/{{end}}">Start writing</a></p></div>{{ end }}
|
||||||
|
|
||||||
<div id="moving"></div>
|
<div id="moving"></div>
|
||||||
|
@ -52,6 +64,25 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ if .Collections }}
|
||||||
|
<div id="move-tmpl">
|
||||||
|
{{if gt (len .Collections) 1}}
|
||||||
|
<div class="action flat-select">
|
||||||
|
<select id="move-POST_ID" onchange="postActions.multiMove(this, 'POST_ID', {{if .SingleUser}}true{{else}}false{{end}})" title="Move this post to one of your blogs">
|
||||||
|
<option style="display:none"></option>
|
||||||
|
{{range .Collections}}<option value="{{.Alias}}">{{.DisplayTitle}}</option>{{end}}
|
||||||
|
</select>
|
||||||
|
<label for="move-POST_ID">move to...</label>
|
||||||
|
<img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" />
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
{{range .Collections}}
|
||||||
|
<a class="action" href="/POST_ID" title="Publish this post to your blog '{{.DisplayTitle}}'" onclick="postActions.move(this, 'POST_ID', '{{.Alias}}', {{if $.SingleUser}}true{{else}}false{{end}});return false">move to {{.DisplayTitle}}</a>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<script src="/js/h.js"></script>
|
<script src="/js/h.js"></script>
|
||||||
<script src="/js/postactions.js"></script>
|
<script src="/js/postactions.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
@ -145,6 +176,50 @@ function postsLoaded(n) {
|
||||||
syncing = true;
|
syncing = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var $loadMore = H.getEl("load-more-p");
|
||||||
|
var curPage = 1;
|
||||||
|
var isLoadingMore = false;
|
||||||
|
function loadMorePosts() {
|
||||||
|
if (isLoadingMore === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var $link = this;
|
||||||
|
isLoadingMore = true;
|
||||||
|
|
||||||
|
$link.className = 'loading';
|
||||||
|
$link.textContent = 'Loading posts...';
|
||||||
|
|
||||||
|
var $posts = H.getEl("anon-posts");
|
||||||
|
|
||||||
|
curPage++;
|
||||||
|
|
||||||
|
var http = new XMLHttpRequest();
|
||||||
|
var url = "/api/me/posts?anonymous=1&page=" + curPage;
|
||||||
|
http.open("GET", url, true);
|
||||||
|
http.setRequestHeader("Content-type", "application/json");
|
||||||
|
http.onreadystatechange = function() {
|
||||||
|
if (http.readyState == 4) {
|
||||||
|
if (http.status == 200) {
|
||||||
|
var data = JSON.parse(http.responseText);
|
||||||
|
for (var i=0; i<data.data.length; i++) {
|
||||||
|
$posts.el.appendChild(createPostEl(data.data[i], true));
|
||||||
|
}
|
||||||
|
if (data.data.length < 10) {
|
||||||
|
$loadMore.el.parentNode.removeChild($loadMore.el);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("Failed to load more posts. Please try again.");
|
||||||
|
curPage--;
|
||||||
|
}
|
||||||
|
isLoadingMore = false;
|
||||||
|
$link.className = '';
|
||||||
|
$link.textContent = 'Load more...';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
http.send();
|
||||||
|
}
|
||||||
|
$loadMore.el.querySelector('a').addEventListener('click', loadMorePosts);
|
||||||
</script>
|
</script>
|
||||||
<script src="/js/posts.js"></script>
|
<script src="/js/posts.js"></script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue