diff --git a/activitypub.go b/activitypub.go index 2bbc7ad..f6f8792 100644 --- a/activitypub.go +++ b/activitypub.go @@ -201,7 +201,7 @@ func handleFetchCollectionOutbox(app *App, w http.ResponseWriter, r *http.Reques ocp := activitystreams.NewOrderedCollectionPage(accountRoot, "outbox", res.TotalPosts, p) ocp.OrderedItems = []interface{}{} - posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false) + posts, err := app.db.GetPosts(app.cfg, c, p, false, true, false, "") for _, pp := range *posts { pp.Collection = res o := pp.ActivityObject(app) diff --git a/app.go b/app.go index 3b4756a..93d359c 100644 --- a/app.go +++ b/app.go @@ -46,9 +46,10 @@ import ( ) const ( - staticDir = "static" - assumedTitleLen = 80 - postsPerPage = 10 + staticDir = "static" + assumedTitleLen = 80 + postsPerPage = 10 + postsPerArchPage = 40 serverSoftware = "WriteFreely" softwareURL = "https://writefreely.org" diff --git a/collections.go b/collections.go index 0ccca2e..90e02ba 100644 --- a/collections.go +++ b/collections.go @@ -608,7 +608,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro } } - ps, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false) + ps, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false, "") if err != nil { return err } @@ -828,15 +828,18 @@ func checkUserForCollection(app *App, cr *collectionReq, r *http.Request, isPost return u, nil } -func newDisplayCollection(c *Collection, cr *collectionReq, page int) *DisplayCollection { +func newDisplayCollection(c *Collection, cr *collectionReq, page int) (*DisplayCollection, error) { coll := &DisplayCollection{ CollectionObj: NewCollectionObj(c), CurrentPage: page, Prefix: cr.prefix, IsTopLevel: isSingleUser, } - c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner) - return coll + err := c.db.GetPostsCount(coll.CollectionObj, cr.isCollOwner) + if err != nil { + return nil, err + } + return coll, nil } // getCollectionPage returns the collection page as an int. If the parsed page value is not @@ -888,9 +891,23 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro // Fetch extra data about the Collection // TODO: refactor out this logic, shared in collection.go:fetchCollection() - coll := newDisplayCollection(c, cr, page) + coll, err := newDisplayCollection(c, cr, page) + if err != nil { + return err + } - coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(coll.Format.PostsPerPage()))) + var ct PostType + if isArchiveView(r) { + ct = postArch + } + + // FIXME: this number will be off when user has pinned posts but isn't a Pro user + ppp := coll.Format.PostsPerPage() + if ct == postArch { + ppp = postsPerArchPage + } + + coll.TotalPages = int(math.Ceil(float64(coll.TotalPosts) / float64(ppp))) if coll.TotalPages > 0 && page > coll.TotalPages { redirURL := fmt.Sprintf("/page/%d", coll.TotalPages) if !app.cfg.App.SingleUser { @@ -899,7 +916,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return impart.HTTPError{http.StatusFound, redirURL} } - coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false, "") // Serve collection displayPage := CollectionPage{ @@ -958,6 +975,9 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro collTmpl := "collection" if app.cfg.App.Chorus { collTmpl = "chorus-collection" + } else if isArchiveView(r) { + displayPage.NavSuffix = "/archive/" + collTmpl = "collection-archive" } err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { @@ -984,6 +1004,10 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return err } +func isArchiveView(r *http.Request) bool { + return strings.HasSuffix(r.RequestURI, "/archive/") || mux.Vars(r)["archive"] == "archive" +} + func handleViewMention(app *App, w http.ResponseWriter, r *http.Request) error { vars := mux.Vars(r) handle := vars["handle"] @@ -1019,7 +1043,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e return err } - coll := newDisplayCollection(c, cr, page) + coll, _ := newDisplayCollection(c, cr, page) taggedPostIDs, err := app.db.GetAllPostsTaggedIDs(c, tag, cr.isCollOwner) if err != nil { @@ -1117,7 +1141,7 @@ func handleViewCollectionLang(app *App, w http.ResponseWriter, r *http.Request) return err } - coll := newDisplayCollection(c, cr, page) + coll, _ := newDisplayCollection(c, cr, page) coll.Language = lang coll.NavSuffix = fmt.Sprintf("/lang:%s", lang) diff --git a/database.go b/database.go index d715fd4..84898e0 100644 --- a/database.go +++ b/database.go @@ -19,6 +19,8 @@ import ( "strings" "time" + "github.com/writeas/monday" + "github.com/go-sql-driver/mysql" "github.com/writeas/web-core/silobridge" wf_db "github.com/writefreely/writefreely/db" @@ -116,8 +118,8 @@ type writestore interface { ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) GetPostLikeCounts(postID string) (int64, error) - GetPostsCount(c *CollectionObj, includeFuture bool) - GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) + GetPostsCount(c *CollectionObj, includeFuture bool) error + GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error) GetAllPostsTaggedIDs(c *Collection, tag string, includeFuture bool) ([]string, error) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) @@ -1258,7 +1260,7 @@ func (db *datastore) GetPostLikeCounts(postID string) (int64, error) { // GetPostsCount modifies the CollectionObj to include the correct number of // standard (non-pinned) posts. It will return future posts if `includeFuture` // is true. -func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { +func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) error { var count int64 timeCondition := "" if !includeFuture { @@ -1271,16 +1273,18 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { case err != nil: log.Error("Failed selecting from collections: %v", err) c.TotalPosts = 0 + return err } c.TotalPosts = int(count) + return nil } // GetPosts retrieves all posts for the given Collection. // It will return future posts if `includeFuture` is true. // It will include only standard (non-pinned) posts unless `includePinned` is true. // TODO: change includeFuture to isOwner, since that's how it's used -func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) { +func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool, contentType PostType) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() @@ -1294,6 +1298,9 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu if page == 0 { start = 0 pagePosts = 1000 + } else if contentType == postArch { + pagePosts = postsPerArchPage + start = page*pagePosts - pagePosts } limitStr := "" @@ -1308,6 +1315,7 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu if !includePinned { pinnedCondition = "AND pinned_position IS NULL" } + // FUTURE: handle different post contentType's here rows, err := db.Query("SELECT "+postCols+" FROM posts WHERE collection_id = ? "+pinnedCondition+" "+timeCondition+" ORDER BY created "+order+limitStr, collID) if err != nil { log.Error("Failed selecting from posts: %v", err) @@ -1328,7 +1336,13 @@ func (db *datastore) GetPosts(cfg *config.Config, c *Collection, page int, inclu p.augmentContent(c) p.formatContent(cfg, c, includeFuture, false) - posts = append(posts, p.processPost()) + pubPost := p.processPost() + if contentType == postArch { + // Overwrite DisplayDate with special Archive page version + loc := monday.FuzzyLocale(pubPost.Language.String) + pubPost.DisplayDate = monday.Format(pubPost.Created, monday.LongNoYrFormatsByLocale[loc], loc) + } + posts = append(posts, pubPost) } err = rows.Err() if err != nil { @@ -2001,7 +2015,7 @@ func (db *datastore) GetMeStats(u *User) userMeStats { func (db *datastore) GetTotalCollections() (collCount int64, err error) { err = db.QueryRow(` - SELECT COUNT(*) + SELECT COUNT(*) FROM collections c LEFT JOIN users u ON u.id = c.owner_id WHERE u.status = 0`).Scan(&collCount) @@ -3127,10 +3141,10 @@ func (db *datastore) GetEmailSubscribers(collID int64, reqConfirmed bool) ([]*Em if reqConfirmed { cond = " AND confirmed = 1" } - rows, err := db.Query(`SELECT s.id, collection_id, user_id, s.email, u.email, subscribed, token, confirmed, allow_export -FROM emailsubscribers s -LEFT JOIN users u - ON u.id = user_id + rows, err := db.Query(`SELECT s.id, collection_id, user_id, s.email, u.email, subscribed, token, confirmed, allow_export +FROM emailsubscribers s +LEFT JOIN users u + ON u.id = user_id WHERE collection_id = ?`+cond+` ORDER BY subscribed DESC`, collID) if err != nil { diff --git a/export.go b/export.go index e6a09c1..a89ed7d 100644 --- a/export.go +++ b/export.go @@ -119,7 +119,7 @@ func compileFullExport(app *App, u *User) *ExportUser { var collObjs []CollectionObj for _, c := range *colls { co := &CollectionObj{Collection: c} - co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true) + co.Posts, err = app.db.GetPosts(app.cfg, &c, 0, true, false, true, "") if err != nil { log.Error("unable to get collection posts: %v", err) } diff --git a/feed.go b/feed.go index 68dd2c0..ae27b3a 100644 --- a/feed.go +++ b/feed.go @@ -67,7 +67,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { if tag != "" { coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false) } else { - coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false, "") } author := "" diff --git a/gopher.go b/gopher.go index 2ac1590..45bbec4 100644 --- a/gopher.go +++ b/gopher.go @@ -111,7 +111,7 @@ func handleGopherCollection(app *App, w gopher.ResponseWriter, r *gopher.Request w.WriteInfo(c.Description) } - posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false) + posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false, "") if err != nil { return err } diff --git a/less/core.less b/less/core.less index a64056a..6dde923 100644 --- a/less/core.less +++ b/less/core.less @@ -5,7 +5,7 @@ body { -moz-osx-font-smoothing: grayscale; background-color: white; color: #111; - + h1, header h2 { a { color: @headerTextColor; @@ -672,6 +672,26 @@ body#collection article, body#subpage article { } } } +#wrapper.archive { + h1 { + margin: 0 !important; + } + ul { + list-style: none; + + li { + display: flex; + justify-content: space-between; + line-height: 1.4; + margin: 0.5em 0; + } + + .year { + font-weight: bold; + font-size: 1.5em; + } + } +} body#post article { p.badge { font-size: 0.9em; @@ -1101,7 +1121,7 @@ li { } } -ul.errors { +ul.errors { padding: 0; text-indent: 0; li.urgent { @@ -1637,4 +1657,4 @@ p#emailsub { font-weight: bold; margin-left: 0.25em; } -} \ No newline at end of file +} diff --git a/less/post-temp.less b/less/post-temp.less index aec7d26..bb818c1 100644 --- a/less/post-temp.less +++ b/less/post-temp.less @@ -12,7 +12,7 @@ body { &:hover { .opacity(1); } - + h1 { font-size: 1.6em; } @@ -30,7 +30,7 @@ body { } } -article, pre, .hljs { +article, pre, .hljs, #wrapper.archive ul { padding: 0.5em 2rem 1.5em; } body#post article, pre, .hljs { diff --git a/posts.go b/posts.go index 1cfc1f0..f4ab8b1 100644 --- a/posts.go +++ b/posts.go @@ -49,6 +49,12 @@ const ( postIDLen = 10 postMetaDateFormat = "2006-01-02 15:04:05" +) + +type PostType string + +const ( + postArch PostType = "archive" shortCodePaid = "" ) @@ -1510,6 +1516,10 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error // User tried to access blog feed without a trailing slash, and // there's no post with a slug "feed" return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "feed/"} + } else if slug == "archive" { + // User tried to access blog Archive without a trailing slash, and + // there's no post with a slug "archive" + return impart.HTTPError{http.StatusFound, c.CanonicalURL() + "archive/"} } po := &Post{ diff --git a/routes.go b/routes.go index 48d8ba6..a5b0f2f 100644 --- a/routes.go +++ b/routes.go @@ -221,6 +221,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { func RouteCollections(handler *Handler, r *mux.Router) { r.HandleFunc("/logout", handler.Web(handleLogOutCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) + r.HandleFunc("/archive/", handler.Web(handleViewCollection, UserLevelReader)) + r.HandleFunc("/{archive:archive}/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) r.HandleFunc("/lang:{lang:[a-z]{2}}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/lang:{lang:[a-z]{2}}/page/{page:[0-9]+}", handler.Web(handleViewCollectionLang, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) diff --git a/sitemap.go b/sitemap.go index 0bbcefb..22e4bb4 100644 --- a/sitemap.go +++ b/sitemap.go @@ -66,7 +66,7 @@ func handleViewSitemap(app *App, w http.ResponseWriter, r *http.Request) error { host = c.CanonicalURL() sm := buildSitemap(host, pre) - posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false) + posts, err := app.db.GetPosts(app.cfg, c, 0, false, false, false, "") if err != nil { log.Error("Error getting posts: %v", err) return err diff --git a/templates.go b/templates.go index 3bb7d13..484bb99 100644 --- a/templates.go +++ b/templates.go @@ -14,8 +14,8 @@ import ( "errors" "html/template" "io" - "os" "net/http" + "os" "path/filepath" "strings" @@ -70,14 +70,14 @@ func initTemplate(parentDir, name string) { filepath.Join(parentDir, templatesDir, "base.tmpl"), filepath.Join(parentDir, templatesDir, "user", "include", "silenced.tmpl"), } - if name == "collection" || name == "collection-tags" || name == "chorus-collection" || name == "read" { + if name == "collection" || name == "collection-tags" || name == "collection-archive" || name == "chorus-collection" || name == "read" { // These pages list out collection posts, so we also parse templatesDir + "include/posts.tmpl" files = append(files, filepath.Join(parentDir, templatesDir, "include", "posts.tmpl")) } if name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "user", "include", "header.tmpl")) } - if name == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { + if name == "collection" || name == "collection-tags" || name == "collection-archive" || name == "collection-post" || name == "post" || name == "chorus-collection" || name == "chorus-collection-post" { files = append(files, filepath.Join(parentDir, templatesDir, "include", "post-render.tmpl")) } templates[name] = template.Must(template.New("").Funcs(funcMap).ParseFiles(files...)) diff --git a/templates/collection-archive.tmpl b/templates/collection-archive.tmpl new file mode 100644 index 0000000..5256c58 --- /dev/null +++ b/templates/collection-archive.tmpl @@ -0,0 +1,118 @@ +{{define "collection"}} + +
+ + +{{.Flash}}
+