Merge pull request #356 from writefreely/draft-list-paging

Draft list paging
This commit is contained in:
Matt Baer 2021-05-04 09:39:22 -04:00 committed by GitHub
commit 73450a50e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 151 additions and 11 deletions

View file

@ -16,6 +16,7 @@ import (
"html/template"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
@ -691,6 +692,22 @@ func viewMyPostsAPI(app *App, u *User, w http.ResponseWriter, r *http.Request) e
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
p := GetPostsCache(u.ID)
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 {
p, err := app.db.GetAnonymousPosts(u)
p, err := app.db.GetAnonymousPosts(u, 1)
if err != nil {
log.Error("unable to fetch anon posts: %v", err)
}

View file

@ -77,7 +77,7 @@ type writestore interface {
GetTotalCollections() (int64, error)
GetTotalPosts() (int64, 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)
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
}
func (db *datastore) GetAnonymousPosts(u *User) (*[]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)
func (db *datastore) GetAnonymousPosts(u *User, page int) (*[]PublicPost, error) {
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 {
log.Error("Failed selecting from posts: %v", err)
return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve user anonymous posts."}

View file

@ -110,7 +110,7 @@ func compileFullExport(app *App, u *User) *ExportUser {
log.Error("unable to fetch collections: %v", err)
}
posts, err := app.db.GetAnonymousPosts(u)
posts, err := app.db.GetAnonymousPosts(u, 0)
if err != nil {
log.Error("unable to fetch anon posts: %v", err)
}

View file

@ -287,6 +287,26 @@ func (h *Handler) UserAPI(f userHandlerFunc) http.HandlerFunc {
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 {
return func(w http.ResponseWriter, r *http.Request) {
handleFunc := func() error {

View file

@ -115,7 +115,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/api/me", handler.All(viewMeAPI)).Methods("GET")
apiMe := write.PathPrefix("/api/me/").Subrouter()
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("/password", handler.All(updatePassphrase)).Methods("POST")
apiMe.HandleFunc("/self", handler.All(updateSettings)).Methods("POST")

View file

@ -181,9 +181,17 @@ var localPosts = function() {
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 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, "&lt;");
$post.id = 'post-' + post.id;
$post.className = 'post';
@ -194,13 +202,22 @@ var createPostEl = function(post) {
posted = getFormattedDate(new Date(post.created))
}
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) {
$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) {
// TODO: switch to using p.summary, after ensuring it matches summary generated on the backend.
$post.innerHTML += '<p>' + post.summary.replace(/</g, "&lt;") + '</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, "&lt;") + '</p>';
}
return $post;
};

View file

@ -1,5 +1,14 @@
{{define "articles"}}
{{template "header" .}}
<style type="text/css">
a.loading {
font-style: italic;
color: #666;
}
#move-tmpl {
display: none;
}
</style>
<div class="snug content-container">
@ -15,7 +24,7 @@
{{ 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>
<div class="atoms posts">
<div id="anon-posts" class="atoms posts">
{{ range $el := .AnonymousPosts }}<div id="post-{{.ID}}" class="post">
<h3><a href="/{{if $.SingleUser}}d/{{end}}{{.ID}}" itemprop="url">{{.DisplayTitle}}</a></h3>
<h4>
@ -39,9 +48,12 @@
</h4>
{{if .Summary}}<p>{{.SummaryHTML}}</p>{{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>
{{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 }}
<div id="moving"></div>
@ -52,6 +64,25 @@
</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/postactions.js"></script>
<script>
@ -145,6 +176,50 @@ function postsLoaded(n) {
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 src="/js/posts.js"></script>