diff --git a/account.go b/account.go index 49700b4..c3d55ba 100644 --- a/account.go +++ b/account.go @@ -28,6 +28,7 @@ import ( "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" + "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" ) @@ -59,11 +60,15 @@ func NewUserPage(app *App, r *http.Request, u *User, title string, flashes []str up.Flashes = flashes up.Path = r.URL.Path up.IsAdmin = u.IsAdmin() - up.CanInvite = app.cfg.App.UserInvites != "" && - (up.IsAdmin || app.cfg.App.UserInvites != "admin") + up.CanInvite = canUserInvite(app.cfg, up.IsAdmin) return up } +func canUserInvite(cfg *config.Config, isAdmin bool) bool { + return cfg.App.UserInvites != "" && + (isAdmin || cfg.App.UserInvites != "admin") +} + func (up *UserPage) SetMessaging(u *User) { //up.NeedsAuth = app.db.DoesUserNeedAuth(u.ID) } @@ -305,10 +310,10 @@ func viewLogin(app *App, w http.ResponseWriter, r *http.Request) error { p := &struct { page.StaticPage - To string - Message template.HTML - Flashes []template.HTML - Username string + To string + Message template.HTML + Flashes []template.HTML + LoginUsername string }{ pageForReq(app, r), r.FormValue("to"), @@ -547,7 +552,7 @@ func getVerboseAuthUser(app *App, token string, u *User, verbose bool) *AuthUser if err != nil { log.Error("Login: Unable to get user posts: %v", err) } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("Login: Unable to get user collections: %v", err) } @@ -717,7 +722,7 @@ func viewMyCollectionsAPI(app *App, u *User, w http.ResponseWriter, r *http.Requ return ErrBadRequestedType } - p, err := app.db.GetCollections(u) + p, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { return err } @@ -740,7 +745,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err log.Error("unable to fetch flashes: %v", err) } - c, err := app.db.GetPublishableCollections(u) + c, err := app.db.GetPublishableCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -763,7 +768,7 @@ func viewArticles(app *App, u *User, w http.ResponseWriter, r *http.Request) err } func viewCollections(app *App, u *User, w http.ResponseWriter, r *http.Request) error { - c, err := app.db.GetCollections(u) + c, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) return fmt.Errorf("No collections") diff --git a/activitypub.go b/activitypub.go index c10838d..06b0468 100644 --- a/activitypub.go +++ b/activitypub.go @@ -145,10 +145,10 @@ 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(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() + o := pp.ActivityObject(app.cfg) a := activitystreams.NewCreateActivity(o) ocp.OrderedItems = append(ocp.OrderedItems, *a) } @@ -564,7 +564,7 @@ func deleteFederatedPost(app *App, p *PublicPost, collID int64) error { } p.Collection.hostName = app.cfg.App.Host actor := p.Collection.PersonObject(collID) - na := p.ActivityObject() + na := p.ActivityObject(app.cfg) // Add followers p.Collection.ID = collID @@ -610,7 +610,7 @@ func federatePost(app *App, p *PublicPost, collID int64, isUpdate bool) error { } } actor := p.Collection.PersonObject(collID) - na := p.ActivityObject() + na := p.ActivityObject(app.cfg) // Add followers p.Collection.ID = collID diff --git a/admin.go b/admin.go index dc8580d..1d94987 100644 --- a/admin.go +++ b/admin.go @@ -18,11 +18,11 @@ import ( "strconv" "time" - "github.com/gogits/gogs/pkg/tool" "github.com/gorilla/mux" "github.com/writeas/impart" "github.com/writeas/web-core/auth" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/appstats" "github.com/writeas/writefreely/config" ) @@ -196,7 +196,7 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque p.LastPost = lp.Format("January 2, 2006, 3:04 PM") } - colls, err := app.db.GetCollections(p.User) + colls, err := app.db.GetCollections(p.User, app.cfg.App.Host) if err != nil { return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user's collections: %v", err)} } @@ -345,6 +345,8 @@ func handleViewAdminPage(app *App, u *User, w http.ResponseWriter, r *http.Reque } p.Content, err = getLandingBody(app) p.Content.ID = "landing" + } else if slug == "reader" { + p.Content, err = getReaderSection(app) } else { p.Content, err = app.db.GetDynamicContent(slug) } @@ -368,7 +370,7 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req id := vars["page"] // Validate - if id != "about" && id != "privacy" && id != "landing" { + if id != "about" && id != "privacy" && id != "landing" && id != "reader" { return impart.HTTPError{http.StatusNotFound, "No such page."} } @@ -382,6 +384,9 @@ func handleAdminUpdateSite(app *App, u *User, w http.ResponseWriter, r *http.Req return impart.HTTPError{http.StatusFound, "/admin/page/" + id + m} } err = app.db.UpdateDynamicContent("landing-body", "", r.FormValue("content"), "section") + } else if id == "reader" { + // Update sections with titles + err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "section") } else { // Update page err = app.db.UpdateDynamicContent(id, r.FormValue("title"), r.FormValue("content"), "page") @@ -428,37 +433,37 @@ func handleAdminUpdateConfig(apper Apper, u *User, w http.ResponseWriter, r *htt } func updateAppStats() { - sysStatus.Uptime = tool.TimeSincePro(appStartTime) + sysStatus.Uptime = appstats.TimeSincePro(appStartTime) m := new(runtime.MemStats) runtime.ReadMemStats(m) sysStatus.NumGoroutine = runtime.NumGoroutine() - sysStatus.MemAllocated = tool.FileSize(int64(m.Alloc)) - sysStatus.MemTotal = tool.FileSize(int64(m.TotalAlloc)) - sysStatus.MemSys = tool.FileSize(int64(m.Sys)) + sysStatus.MemAllocated = appstats.FileSize(int64(m.Alloc)) + sysStatus.MemTotal = appstats.FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = appstats.FileSize(int64(m.Sys)) sysStatus.Lookups = m.Lookups sysStatus.MemMallocs = m.Mallocs sysStatus.MemFrees = m.Frees - sysStatus.HeapAlloc = tool.FileSize(int64(m.HeapAlloc)) - sysStatus.HeapSys = tool.FileSize(int64(m.HeapSys)) - sysStatus.HeapIdle = tool.FileSize(int64(m.HeapIdle)) - sysStatus.HeapInuse = tool.FileSize(int64(m.HeapInuse)) - sysStatus.HeapReleased = tool.FileSize(int64(m.HeapReleased)) + sysStatus.HeapAlloc = appstats.FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = appstats.FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = appstats.FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = appstats.FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = appstats.FileSize(int64(m.HeapReleased)) sysStatus.HeapObjects = m.HeapObjects - sysStatus.StackInuse = tool.FileSize(int64(m.StackInuse)) - sysStatus.StackSys = tool.FileSize(int64(m.StackSys)) - sysStatus.MSpanInuse = tool.FileSize(int64(m.MSpanInuse)) - sysStatus.MSpanSys = tool.FileSize(int64(m.MSpanSys)) - sysStatus.MCacheInuse = tool.FileSize(int64(m.MCacheInuse)) - sysStatus.MCacheSys = tool.FileSize(int64(m.MCacheSys)) - sysStatus.BuckHashSys = tool.FileSize(int64(m.BuckHashSys)) - sysStatus.GCSys = tool.FileSize(int64(m.GCSys)) - sysStatus.OtherSys = tool.FileSize(int64(m.OtherSys)) - - sysStatus.NextGC = tool.FileSize(int64(m.NextGC)) + sysStatus.StackInuse = appstats.FileSize(int64(m.StackInuse)) + sysStatus.StackSys = appstats.FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = appstats.FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = appstats.FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = appstats.FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = appstats.FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = appstats.FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = appstats.FileSize(int64(m.GCSys)) + sysStatus.OtherSys = appstats.FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = appstats.FileSize(int64(m.NextGC)) sysStatus.LastGC = fmt.Sprintf("%.1fs", float64(time.Now().UnixNano()-int64(m.LastGC))/1000/1000/1000) sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) diff --git a/app.go b/app.go index c52f59d..514c7c8 100644 --- a/app.go +++ b/app.go @@ -185,8 +185,8 @@ func (app *App) ReqLog(r *http.Request, status int, timeSince time.Duration) str return fmt.Sprintf("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, timeSince, r.UserAgent()) } -// handleViewHome shows page at root path. Will be the Pad if logged in and the -// catch-all landing page otherwise. +// handleViewHome shows page at root path. It checks the configuration and +// authentication state to show the correct page. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if app.cfg.App.SingleUser { // Render blog index @@ -198,6 +198,15 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { if !forceLanding { // Show correct page based on user auth status and configured landing path u := getUserSession(app, r) + + if app.cfg.App.Chorus { + // This instance is focused on reading, so show Reader on home route if not + // private or a private-instance user is logged in. + if !app.cfg.App.Private || u != nil { + return viewLocalTimeline(app, w, r) + } + } + if u != nil { // User is logged in, so show the Pad return handleViewPad(app, w, r) @@ -208,6 +217,12 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { } } + return handleViewLanding(app, w, r) +} + +func handleViewLanding(app *App, w http.ResponseWriter, r *http.Request) error { + forceLanding := r.FormValue("landing") == "1" + p := struct { page.StaticPage Flashes []template.HTML @@ -225,14 +240,14 @@ func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { log.Error("unable to get landing banner: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get banner: %v", err)} } - p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "")) + p.Banner = template.HTML(applyMarkdown([]byte(banner.Content), "", app.cfg)) content, err := getLandingBody(app) if err != nil { log.Error("unable to get landing content: %v", err) return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get content: %v", err)} } - p.Content = template.HTML(applyMarkdown([]byte(content.Content), "")) + p.Content = template.HTML(applyMarkdown([]byte(content.Content), "", app.cfg)) // Get error messages session, err := app.sessionStore.Get(r, cookieName) @@ -280,7 +295,7 @@ func handleTemplatedPage(app *App, w http.ResponseWriter, r *http.Request, t *te return err } p.ContentTitle = c.Title.String - p.Content = template.HTML(applyMarkdown([]byte(c.Content), "")) + p.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) p.PlainContent = shortPostDescription(stripmd.Strip(c.Content)) if !c.Updated.IsZero() { p.Updated = c.Updated.Format("January 2, 2006") @@ -318,6 +333,8 @@ func pageForReq(app *App, r *http.Request) page.StaticPage { u = getUserSession(app, r) if u != nil { p.Username = u.Username + p.IsAdmin = u != nil && u.IsAdmin() + p.CanInvite = canUserInvite(app.cfg, p.IsAdmin) } } p.CanViewReader = !app.cfg.App.Private || u != nil diff --git a/appstats/appstats.go b/appstats/appstats.go new file mode 100644 index 0000000..bf27c7b --- /dev/null +++ b/appstats/appstats.go @@ -0,0 +1,128 @@ +// Copyright 2014-2018 The Gogs Authors. All rights reserved. +// Use of this source code is governed by a MIT-style license that can be +// found in the LICENSE file of the Gogs project (github.com/gogs/gogs). + +package appstats + +import ( + "fmt" + "math" + "strings" + "time" +) + +// Borrowed from github.com/gogs/gogs/pkg/tool + +// Seconds-based time units +const ( + Minute = 60 + Hour = 60 * Minute + Day = 24 * Hour + Week = 7 * Day + Month = 30 * Day + Year = 12 * Month +) + +func computeTimeDiff(diff int64) (int64, string) { + diffStr := "" + switch { + case diff <= 0: + diff = 0 + diffStr = "now" + case diff < 2: + diff = 0 + diffStr = "1 second" + case diff < 1*Minute: + diffStr = fmt.Sprintf("%d seconds", diff) + diff = 0 + + case diff < 2*Minute: + diff -= 1 * Minute + diffStr = "1 minute" + case diff < 1*Hour: + diffStr = fmt.Sprintf("%d minutes", diff/Minute) + diff -= diff / Minute * Minute + + case diff < 2*Hour: + diff -= 1 * Hour + diffStr = "1 hour" + case diff < 1*Day: + diffStr = fmt.Sprintf("%d hours", diff/Hour) + diff -= diff / Hour * Hour + + case diff < 2*Day: + diff -= 1 * Day + diffStr = "1 day" + case diff < 1*Week: + diffStr = fmt.Sprintf("%d days", diff/Day) + diff -= diff / Day * Day + + case diff < 2*Week: + diff -= 1 * Week + diffStr = "1 week" + case diff < 1*Month: + diffStr = fmt.Sprintf("%d weeks", diff/Week) + diff -= diff / Week * Week + + case diff < 2*Month: + diff -= 1 * Month + diffStr = "1 month" + case diff < 1*Year: + diffStr = fmt.Sprintf("%d months", diff/Month) + diff -= diff / Month * Month + + case diff < 2*Year: + diff -= 1 * Year + diffStr = "1 year" + default: + diffStr = fmt.Sprintf("%d years", diff/Year) + diff = 0 + } + return diff, diffStr +} + +// TimeSincePro calculates the time interval and generate full user-friendly string. +func TimeSincePro(then time.Time) string { + now := time.Now() + diff := now.Unix() - then.Unix() + + if then.After(now) { + return "future" + } + + var timeStr, diffStr string + for { + if diff == 0 { + break + } + + diff, diffStr = computeTimeDiff(diff) + timeStr += ", " + diffStr + } + return strings.TrimPrefix(timeStr, ", ") +} + +func logn(n, b float64) float64 { + return math.Log(n) / math.Log(b) +} + +func humanateBytes(s uint64, base float64, sizes []string) string { + if s < 10 { + return fmt.Sprintf("%d B", s) + } + e := math.Floor(logn(float64(s), base)) + suffix := sizes[int(e)] + val := float64(s) / math.Pow(base, math.Floor(e)) + f := "%.0f" + if val < 10 { + f = "%.1f" + } + + return fmt.Sprintf(f+" %s", val, suffix) +} + +// FileSize calculates the file size and generate user-friendly string. +func FileSize(s int64) string { + sizes := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} + return humanateBytes(uint64(s), 1024, sizes) +} diff --git a/collections.go b/collections.go index 95dd808..c4cefeb 100644 --- a/collections.go +++ b/collections.go @@ -521,7 +521,7 @@ func fetchCollectionPosts(app *App, w http.ResponseWriter, r *http.Request) erro } } - posts, err := app.db.GetPosts(c, page, isCollOwner, false, false) + posts, err := app.db.GetPosts(app.cfg, c, page, isCollOwner, false, false) if err != nil { return err } @@ -550,6 +550,8 @@ type CollectionPage struct { Username string Collections *[]Collection PinnedPosts *[]PublicPost + IsAdmin bool + CanInvite bool } func (c *CollectionObj) ScriptDisplay() template.JS { @@ -732,6 +734,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro if c == nil || err != nil { return err } + c.hostName = app.cfg.App.Host suspended, err := app.db.IsUserSuspended(c.OwnerID) if err != nil { @@ -742,6 +745,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro if suspended { return ErrCollectionNotFound } + // Serve ActivityStreams data now, if requested if strings.Contains(r.Header.Get("Accept"), "application/activity+json") { ac := c.PersonObject() @@ -762,7 +766,7 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro return impart.HTTPError{http.StatusFound, redirURL} } - coll.Posts, _ = app.db.GetPosts(c, page, cr.isCollOwner, false, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, page, cr.isCollOwner, false, false) // Serve collection displayPage := CollectionPage{ @@ -771,6 +775,8 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro IsCustomDomain: cr.isCustomDomain, IsWelcome: r.FormValue("greeting") != "", } + displayPage.IsAdmin = u != nil && u.IsAdmin() + displayPage.CanInvite = canUserInvite(app.cfg, displayPage.IsAdmin) var owner *User if u != nil { displayPage.Username = u.Username @@ -780,14 +786,15 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } - if owner == nil { + isOwner := owner != nil + if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { @@ -800,9 +807,13 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) - err = templates["collection"].ExecuteTemplate(w, "collection", displayPage) + collTmpl := "collection" + if app.cfg.App.Chorus { + collTmpl = "chorus-collection" + } + err = templates[collTmpl].ExecuteTemplate(w, "collection", displayPage) if err != nil { log.Error("Unable to render collection index: %v", err) } @@ -855,7 +866,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll := newDisplayCollection(c, cr, page) - coll.Posts, _ = app.db.GetPostsTagged(c, tag, page, cr.isCollOwner) + coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, page, cr.isCollOwner) if coll.Posts != nil && len(*coll.Posts) == 0 { return ErrCollectionPageNotFound } @@ -881,14 +892,15 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e owner = u displayPage.CanPin = true - pubColls, err := app.db.GetPublishableCollections(owner) + pubColls, err := app.db.GetPublishableCollections(owner, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } displayPage.Collections = pubColls } } - if owner == nil { + isOwner := owner != nil + if !isOwner { // Current user doesn't own collection; retrieve owner information owner, err = app.db.GetUserByID(coll.OwnerID) if err != nil { @@ -900,7 +912,7 @@ func handleViewCollectionTag(app *App, w http.ResponseWriter, r *http.Request) e coll.Owner = displayPage.Owner // Add more data // TODO: fix this mess of collections inside collections - displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj) + displayPage.PinnedPosts, _ = app.db.GetPinnedPosts(coll.CollectionObj, isOwner) err = templates["collection-tags"].ExecuteTemplate(w, "collection-tags", displayPage) if err != nil { diff --git a/config/config.go b/config/config.go index e27ffb9..84bae86 100644 --- a/config/config.go +++ b/config/config.go @@ -68,8 +68,13 @@ type ( JSDisabled bool `ini:"disable_js"` WebFonts bool `ini:"webfonts"` Landing string `ini:"landing"` + SimpleNav bool `ini:"simple_nav"` WFModesty bool `ini:"wf_modesty"` + // Site functionality + Chorus bool `ini:"chorus"` + DisableDrafts bool `ini:"disable_drafts"` + // Users SingleUser bool `ini:"single_user"` OpenRegistration bool `ini:"open_registration"` diff --git a/database.go b/database.go index 150a74f..b465e68 100644 --- a/database.go +++ b/database.go @@ -65,8 +65,8 @@ type writestore interface { ChangeSettings(app *App, u *User, s *userSettings) error ChangePassphrase(userID int64, sudo bool, curPass string, hashedPass []byte) error - GetCollections(u *User) (*[]Collection, error) - GetPublishableCollections(u *User) (*[]Collection, error) + GetCollections(u *User, hostName string) (*[]Collection, error) + GetPublishableCollections(u *User, hostName string) (*[]Collection, error) GetMeStats(u *User) userMeStats GetTotalCollections() (int64, error) GetTotalPosts() (int64, error) @@ -94,7 +94,7 @@ type writestore interface { UpdatePostPinState(pinned bool, postID string, collID, ownerID, pos int64) error GetLastPinnedPostPos(collID int64) int64 - GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) + GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) RemoveCollectionRedirect(t *sql.Tx, alias string) error GetCollectionRedirect(alias string) (new string) IsCollectionAttributeOn(id int64, attr string) bool @@ -106,8 +106,8 @@ type writestore interface { ClaimPosts(cfg *config.Config, userID int64, collAlias string, posts *[]ClaimPostRequest) (*[]ClaimPostResult, error) GetPostsCount(c *CollectionObj, includeFuture bool) - GetPosts(c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) - GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) + GetPosts(cfg *config.Config, c *Collection, page int, includeFuture, forceRecentFirst, includePinned bool) (*[]PublicPost, error) + GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) GetAPFollowers(c *Collection) (*[]RemoteUser, error) GetAPActorKeys(collectionID int64) ([]byte, []byte) @@ -1087,7 +1087,7 @@ func (db *datastore) GetPostsCount(c *CollectionObj, includeFuture bool) { // 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(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) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() @@ -1132,7 +1132,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen break } p.extractData() - p.formatContent(c, includeFuture) + p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) } @@ -1148,7 +1148,7 @@ func (db *datastore) GetPosts(c *Collection, page int, includeFuture, forceRecen // given tag. // It will return future posts if `includeFuture` is true. // TODO: change includeFuture to isOwner, since that's how it's used -func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { +func (db *datastore) GetPostsTagged(cfg *config.Config, c *Collection, tag string, page int, includeFuture bool) (*[]PublicPost, error) { collID := c.ID cf := c.NewFormat() @@ -1196,7 +1196,7 @@ func (db *datastore) GetPostsTagged(c *Collection, tag string, page int, include break } p.extractData() - p.formatContent(c, includeFuture) + p.formatContent(cfg, c, includeFuture) posts = append(posts, p.processPost()) } @@ -1550,9 +1550,13 @@ func (db *datastore) GetLastPinnedPostPos(collID int64) int64 { return lastPos.Int64 } -func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) { +func (db *datastore) GetPinnedPosts(coll *CollectionObj, includeFuture bool) (*[]PublicPost, error) { // FIXME: sqlite-backed instances don't include ellipsis on truncated titles - rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL ORDER BY pinned_position ASC", coll.ID) + timeCondition := "" + if !includeFuture { + timeCondition = "AND created <= " + db.now() + } + rows, err := db.Query("SELECT id, slug, title, "+db.clip("content", 80)+", pinned_position FROM posts WHERE collection_id = ? AND pinned_position IS NOT NULL "+timeCondition+" ORDER BY pinned_position ASC", coll.ID) if err != nil { log.Error("Failed selecting pinned posts: %v", err) return nil, impart.HTTPError{http.StatusInternalServerError, "Couldn't retrieve pinned posts."} @@ -1576,7 +1580,7 @@ func (db *datastore) GetPinnedPosts(coll *CollectionObj) (*[]PublicPost, error) return &posts, nil } -func (db *datastore) GetCollections(u *User) (*[]Collection, error) { +func (db *datastore) GetCollections(u *User, hostName string) (*[]Collection, error) { rows, err := db.Query("SELECT id, alias, title, description, privacy, view_count FROM collections WHERE owner_id = ? ORDER BY id ASC", u.ID) if err != nil { log.Error("Failed selecting from collections: %v", err) @@ -1592,6 +1596,7 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { log.Error("Failed scanning row: %v", err) break } + c.hostName = hostName c.URL = c.CanonicalURL() c.Public = c.IsPublic() @@ -1605,8 +1610,8 @@ func (db *datastore) GetCollections(u *User) (*[]Collection, error) { return &colls, nil } -func (db *datastore) GetPublishableCollections(u *User) (*[]Collection, error) { - c, err := db.GetCollections(u) +func (db *datastore) GetPublishableCollections(u *User, hostName string) (*[]Collection, error) { + c, err := db.GetCollections(u, hostName) if err != nil { return nil, err } @@ -2277,6 +2282,19 @@ func (db *datastore) GetUserInvite(id string) (*Invite, error) { return &i, nil } +// IsUsersInvite returns true if the user with ID created the invite with code +// and an error other than sql no rows, if any. Will return false in the event +// of an error. +func (db *datastore) IsUsersInvite(code string, userID int64) (bool, error) { + var id string + err := db.QueryRow("SELECT id FROM userinvites WHERE id = ? AND owner_id = ?", code, userID).Scan(&id) + if err != nil && err != sql.ErrNoRows { + log.Error("Failed selecting invite: %v", err) + return false, err + } + return id != "", nil +} + func (db *datastore) GetUsersInvitedCount(id string) int64 { var count int64 err := db.QueryRow("SELECT COUNT(*) FROM usersinvited WHERE invite_id = ?", id).Scan(&count) diff --git a/export.go b/export.go index 47a2603..3b5ac49 100644 --- a/export.go +++ b/export.go @@ -104,7 +104,7 @@ func compileFullExport(app *App, u *User) *ExportUser { User: u, } - colls, err := app.db.GetCollections(u) + colls, err := app.db.GetCollections(u, app.cfg.App.Host) if err != nil { log.Error("unable to fetch collections: %v", err) } @@ -118,7 +118,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(&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 353b1b9..44bb331 100644 --- a/feed.go +++ b/feed.go @@ -65,9 +65,9 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { tag := mux.Vars(req)["tag"] if tag != "" { - coll.Posts, _ = app.db.GetPostsTagged(c, tag, 1, false) + coll.Posts, _ = app.db.GetPostsTagged(app.cfg, c, tag, 1, false) } else { - coll.Posts, _ = app.db.GetPosts(c, 1, false, true, false) + coll.Posts, _ = app.db.GetPosts(app.cfg, c, 1, false, true, false) } author := "" @@ -104,7 +104,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { Title: title, Link: &Link{Href: permalink}, Description: "", - Content: applyMarkdown([]byte(p.Content), ""), + Content: applyMarkdown([]byte(p.Content), "", app.cfg), Author: &Author{author, ""}, Created: p.Created, Updated: p.Updated, diff --git a/go.mod b/go.mod index 5e040ba..9c67aeb 100644 --- a/go.mod +++ b/go.mod @@ -2,25 +2,14 @@ module github.com/writeas/writefreely require ( github.com/BurntSushi/toml v0.3.1 // indirect - github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f // indirect - github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 // indirect github.com/alecthomas/gometalinter v3.0.0+incompatible // indirect github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect - github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 // indirect github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 // indirect github.com/clbanning/mxj v1.8.4 // indirect github.com/dustin/go-humanize v1.0.0 github.com/fatih/color v1.7.0 - github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 // indirect - github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 // indirect - github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 // indirect github.com/go-sql-driver/mysql v1.4.1 github.com/go-test/deep v1.0.1 // indirect - github.com/gogits/gogs v0.11.86 - github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 // indirect - github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 // indirect - github.com/gogs/gogs v0.11.86 // indirect - github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a // indirect github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect github.com/gorilla/feeds v1.1.0 @@ -28,17 +17,13 @@ require ( github.com/gorilla/schema v1.0.2 github.com/gorilla/sessions v1.1.3 github.com/guregu/null v3.4.0+incompatible - github.com/ikeikeikeike/go-sitemap-generator v1.0.1 github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 - github.com/imdario/mergo v0.3.7 // indirect - github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/manifoldco/promptui v0.3.2 github.com/mattn/go-colorable v0.1.0 // indirect github.com/mattn/go-sqlite3 v1.10.0 - github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa // indirect github.com/microcosm-cc/bluemonday v1.0.2 github.com/mitchellh/go-wordwrap v1.0.0 github.com/nicksnyder/go-i18n v1.10.0 // indirect @@ -46,7 +31,6 @@ require ( github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect - github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect @@ -70,11 +54,7 @@ require ( golang.org/x/tools v0.0.0-20190208222737-3744606dbb67 // indirect google.golang.org/appengine v1.4.0 // indirect gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c // indirect - gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e // indirect - gopkg.in/clog.v1 v1.2.0 // indirect gopkg.in/ini.v1 v1.41.0 - gopkg.in/macaron.v1 v1.3.2 // indirect - gopkg.in/redis.v2 v2.3.2 // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 8898bec..ec1e19d 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f h1:m1tYqjD/N0vF/S8s/ZKz/eccUr8RAAcrOK2MhXeTegA= -github.com/Unknwon/com v0.0.0-20181010210213-41959bdd855f/go.mod h1:KYCjqMOeHpNuTOiFQU6WEcTG7poCJrUs0YgyHNtn1no= -github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966 h1:Mp8GNJ/tdTZIEdLdZfykEJaL3mTyEYrSzYNcdoQKpJk= -github.com/Unknwon/i18n v0.0.0-20171114194641-b64d33658966/go.mod h1:SFtfq0zFPsENI7DpE87QM2hcYu5QQ0fRdCgP+P1Hrqo= github.com/alecthomas/gometalinter v2.0.11+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= github.com/alecthomas/gometalinter v3.0.0+incompatible h1:e9Zfvfytsw/e6Kd/PYd75wggK+/kX5Xn8IYDUKyc5fU= github.com/alecthomas/gometalinter v3.0.0+incompatible/go.mod h1:qfIpQGGz3d+NmgyPBqv+LSh50emm1pt72EtcX2vKYQk= @@ -11,8 +7,6 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZq github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= -github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737 h1:rRISKWyXfVxvoa702s91Zl5oREZTrR3yv+tXrrX7G/g= -github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49 h1:jWNY1NDg6a/c8RSXkai7IX6UOhir0LD39I4Dukg+4Ks= github.com/captncraig/cors v0.0.0-20180620154129-376d45073b49/go.mod h1:EIlIeMufZ8nqdUhnesledB15xLRl4wIJUppwDLPrdrQ= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= @@ -36,26 +30,10 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/go-fed/httpsig v0.1.0/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= -github.com/go-macaron/cache v0.0.0-20151013081102-561735312776 h1:UYIHS1r0WotqB5cIa0PAiV0m6GzD9rDBcn4alp5JgCw= -github.com/go-macaron/cache v0.0.0-20151013081102-561735312776/go.mod h1:hHAsZm/oBZVcY+S7qdQL6Vbg5VrXF6RuKGuqsszt3Ok= -github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191 h1:NjHlg70DuOkcAMqgt0+XA+NHwtu66MkTVVgR4fFWbcI= -github.com/go-macaron/inject v0.0.0-20160627170012-d8a0b8677191/go.mod h1:VFI2o2q9kYsC4o7VP1HrEVosiZZTd+MVT3YZx4gqvJw= -github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193 h1:z/nqwd+ql/r6Q3QGnwNd6B89UjPytM0be5pDQV9TuWw= -github.com/go-macaron/session v0.0.0-20190131233854-0a0a789bf193/go.mod h1:ScEJm9Gk+ez5JJTml5WlBIqavAfuE5nF8e4Gvyz/X+A= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg= github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= -github.com/gogits/gogs v0.11.86 h1:IujCpA+F/mYDXTcqdy593rl2donWakAWoL2HYZn7spw= -github.com/gogits/gogs v0.11.86/go.mod h1:H8FMbPPb+o/TgI6YnmQmT8nmEIHypXDau+f2CChYoCk= -github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs= -github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= -github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09 h1:UdOSIHZpkYcajRbfebBYzFDsL3SuqObH3bvKYBqgKmI= -github.com/gogs/go-libravatar v0.0.0-20161120025154-cd1abbd55d09/go.mod h1:Zas3BtO88pk1cwUfEYlvnl/CRwh0ybDxRWSwRjG8I3w= -github.com/gogs/gogs v0.11.86 h1:D+dXuY/6XjJ2t74W/dxo7ogx5+xW05Va8sJiQSS4WXA= -github.com/gogs/gogs v0.11.86/go.mod h1:qlbvdn16XTC6q7eR+thjW+OLdN+mi2PBZ8KqVT39T88= -github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a h1:8DZwxETOVWIinYxDK+i6L+rMb7eGATGaakD6ZucfHVk= -github.com/gogs/minwinsvc v0.0.0-20170301035411-95be6356811a/go.mod h1:TUIZ+29jodWQ8Gk6Pvtg4E09aMsc3C/VLZiVYfUhWQU= github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 h1:6DVPu65tee05kY0/rciBQ47ue+AnuY8KTayV6VHikIo= github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= @@ -80,14 +58,8 @@ github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9R github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= github.com/guregu/null v3.4.0+incompatible h1:a4mw37gBO7ypcBlTJeZGuMpSxxFTV9qFfFKgWxQSGaM= github.com/guregu/null v3.4.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= -github.com/ikeikeikeike/go-sitemap-generator v1.0.1 h1:49Fn8gro/B12vCY8pf5/+/Jpr3kwB9TvP0MSymo69SY= -github.com/ikeikeikeike/go-sitemap-generator v1.0.1/go.mod h1:QI+zWsz6yQyxkG9LWNcnu0f7aiAE5tPdsZOsICgmd1c= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2 h1:wIdDEle9HEy7vBPjC6oKz6ejs3Ut+jmsYvuOoAW2pSM= github.com/ikeikeikeike/go-sitemap-generator/v2 v2.0.2/go.mod h1:WtaVKD9TeruTED9ydiaOJU08qGoEPP/LyzTKiD3jEsw= -github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI= -github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/jteeuwen/go-bindata v3.0.7+incompatible h1:91Uy4d9SYVr1kyTJ15wJsog+esAZZl7JmEfTkwmhJts= -github.com/jteeuwen/go-bindata v3.0.7+incompatible/go.mod h1:JVvhzYOiGBnFSYRyV00iY8q7/0PThjIYav1p9h5dmKs= github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= @@ -113,8 +85,6 @@ github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o= github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa h1:XvNrttGMJfVrUqblGju4IkjYXwx6l5OAAyjaIsydzsk= -github.com/mcuadros/go-version v0.0.0-20180611085657-6d5863ca60fa/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= @@ -131,8 +101,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca h1:NugYot0LIVPxTvN8n+Kvkn6TrbMyxQiuvKdEwFdR9vI= -github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/smartystreets/assertions v0.0.0-20190116191733-b6c0e53d7304 h1:Jpy1PXuP99tXNrhbq2BaPz9B+jNAvH1JPQQpG/9GCXY= @@ -167,8 +135,6 @@ github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= github.com/writeas/web-core v1.0.0 h1:5VKkCakQgdKZcbfVKJXtRpc5VHrkflusCl/KRCPzpQ0= github.com/writeas/web-core v1.0.0/go.mod h1:Si3chV7VWgY8CsV+3gRolMXSO2Vx1ZFAQ/mkrpvmyEE= -github.com/writefreely/go-nodeinfo v1.1.0 h1:dp/ieEu0/gTeNKFvJTYhzBBouyFn7aiWtWzkb8J1JLg= -github.com/writefreely/go-nodeinfo v1.1.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= github.com/writefreely/go-nodeinfo v1.2.0 h1:La+YbTCvmpTwFhBSlebWDDL81N88Qf/SCAvRLR7F8ss= github.com/writefreely/go-nodeinfo v1.2.0/go.mod h1:UTvE78KpcjYOlRHupZIiSEFcXHioTXuacCbHU+CAcPg= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59 h1:hk3yo72LXLapY9EXVttc3Z1rLOxT9IuAPPX3GpY2+jo= @@ -195,19 +161,11 @@ google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO50 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c h1:vTxShRUnK60yd8DZU+f95p1zSLj814+5CuEh7NjF2/Y= gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20180810215634-df19058c872c/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= -gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e h1:wGA78yza6bu/mWcc4QfBuIEHEtc06xdiU0X8sY36yUU= -gopkg.in/bufio.v1 v1.0.0-20140618132640-567b2bfa514e/go.mod h1:xsQCaysVCudhrYTfzYWe577fCe7Ceci+6qjO2Rdc0Z4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/clog.v1 v1.2.0 h1:BHfwHRNQy497iBNsRBassPixSAxRbn2z5KVkdBFbwxc= -gopkg.in/clog.v1 v1.2.0/go.mod h1:L6fgdpdhFgKX4eGuDvt+N6X2GwZE160NRrIHzvaF8ZM= gopkg.in/ini.v1 v1.41.0 h1:Ka3ViY6gNYSKiVy71zXBEqKplnV35ImDLVG+8uoIklE= gopkg.in/ini.v1 v1.41.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/macaron.v1 v1.3.2 h1:AvWIaPmwBUA87/OWzePkoxeaw6YJWDfBt1pDFPBnLf8= -gopkg.in/macaron.v1 v1.3.2/go.mod h1:PrsiawTWAGZs6wFbT5hlr7SQ2Ns9h7cUVtcUu4lQOVo= -gopkg.in/redis.v2 v2.3.2 h1:GPVIIB/JnL1wvfULefy3qXmPu1nfNu2d0yA09FHgwfs= -gopkg.in/redis.v2 v2.3.2/go.mod h1:4wl9PJ/CqzeHk3LVq1hNLHH8krm3+AXEgut4jVc++LU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/invites.go b/invites.go index 93b82b4..adff49a 100644 --- a/invites.go +++ b/invites.go @@ -119,6 +119,36 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error { return err } + expired := i.Expired() + if !expired && i.MaxUses.Valid && i.MaxUses.Int64 > 0 { + // Invite has a max-use number, so check if we're past that limit + i.uses = app.db.GetUsersInvitedCount(inviteCode) + expired = i.uses >= i.MaxUses.Int64 + } + + if u := getUserSession(app, r); u != nil { + // check if invite belongs to another user + // error can be ignored as not important in this case + if ownInvite, _ := app.db.IsUsersInvite(inviteCode, u.ID); !ownInvite { + addSessionFlash(app, w, r, "You're already registered and logged in.", nil) + // show homepage + return impart.HTTPError{http.StatusFound, "/me/settings"} + } + + // show invite instructions + p := struct { + *UserPage + Invite *Invite + Expired bool + }{ + UserPage: NewUserPage(app, r, u, "Invite to "+app.cfg.App.SiteName, nil), + Invite: i, + Expired: expired, + } + showUserPage(w, "invite-help", p) + return nil + } + p := struct { page.StaticPage Error string @@ -129,16 +159,10 @@ func handleViewInvite(app *App, w http.ResponseWriter, r *http.Request) error { Invite: inviteCode, } - if i.Expired() { + if expired { p.Error = "This invite link has expired." } - if i.MaxUses.Valid && i.MaxUses.Int64 > 0 { - if c := app.db.GetUsersInvitedCount(inviteCode); c >= i.MaxUses.Int64 { - p.Error = "This invite link has expired." - } - } - // Get error messages session, err := app.sessionStore.Get(r, cookieName) if err != nil { diff --git a/less/Makefile b/less/Makefile index e81258a..117e9b2 100644 --- a/less/Makefile +++ b/less/Makefile @@ -1,20 +1,10 @@ -ifeq ($(shell which lessc),/usr/bin/lessc) - LESSC=/usr/bin/lessc -else ifeq ($(shell which lessc),/usr/local/bin/lessc) - LESSC=/usr/local/bin/lessc -else ifeq ($(shell which lessc),/bin/lessc) - LESSC=/bin/lessc -else - LESSC=node_modules/.bin/lessc -endif -export LESSC - CSSDIR=../static/css/ all : - $(LESSC) app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css - $(LESSC) fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css - $(LESSC) icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css + @command -v lessc >/dev/null 2>&1 || { echo >&2 "lessc is not installed, please run: make install or: less/install-less.sh"; exit 1; } + lessc app.less --clean-css="--s1 --advanced" $(CSSDIR)write.css + lessc fonts.less --clean-css="--s1 --advanced" $(CSSDIR)fonts.css + lessc icons.less --clean-css="--s1 --advanced" $(CSSDIR)icons.css install : ./install-less.sh diff --git a/less/core.less b/less/core.less index 7e39f35..f4332a9 100644 --- a/less/core.less +++ b/less/core.less @@ -405,6 +405,31 @@ body { } } +nav#full-nav { + margin: 0; + + .left-side { + display: inline-block; + + a:first-child { + margin-left: 0; + } + } + + .right-side { + float: right; + } +} + +nav#full-nav a.simple-btn, .tool button { + font-family: @sansFont; + border: 1px solid #ccc !important; + padding: .5rem 1rem; + margin: 0; + .rounded(.25em); + text-decoration: none; +} + .post-title { a { &:link { diff --git a/less/pad-theme.less b/less/pad-theme.less index af1f95c..a8f668e 100644 --- a/less/pad-theme.less +++ b/less/pad-theme.less @@ -63,7 +63,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: #000; } } @@ -100,7 +100,7 @@ body#pad, body#pad-sub { } } #belt { - a { + a, button { color: white; } } diff --git a/less/pad.less b/less/pad.less index d37c6bc..a132b30 100644 --- a/less/pad.less +++ b/less/pad.less @@ -222,6 +222,13 @@ body#pad, body#pad-sub { font-style: italic; } } + button { + font-family: @sansFont; + background-color: transparent; + padding-top: 0.25rem; + padding-bottom: 0.25rem; + border: 0; + } } } } diff --git a/pad.go b/pad.go index 8a54b76..db356c4 100644 --- a/pad.go +++ b/pad.go @@ -49,7 +49,7 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } var err error if appData.User != nil { - appData.Blogs, err = app.db.GetPublishableCollections(appData.User) + appData.Blogs, err = app.db.GetPublishableCollections(appData.User, app.cfg.App.Host) if err != nil { log.Error("Unable to get user's blogs for Pad: %v", err) } @@ -60,16 +60,13 @@ func handleViewPad(app *App, w http.ResponseWriter, r *http.Request) error { } padTmpl := app.cfg.App.Editor - if padTmpl == "" { + if templates[padTmpl] == nil { + log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl) padTmpl = "pad" } if action == "" && slug == "" { // Not editing any post; simply render the Pad - if templates[padTmpl] == nil { - log.Info("No template '%s' found. Falling back to default 'pad' template.", padTmpl) - padTmpl = "pad" - } if err = templates[padTmpl].ExecuteTemplate(w, "pad", appData); err != nil { log.Error("Unable to execute template: %v", err) } diff --git a/page/page.go b/page/page.go index 2af5322..15f09a9 100644 --- a/page/page.go +++ b/page/page.go @@ -28,6 +28,8 @@ type StaticPage struct { Values map[string]string Flashes []string CanViewReader bool + IsAdmin bool + CanInvite bool } // SanitizeHost alters the StaticPage to contain a real hostname. This is diff --git a/pages.go b/pages.go index 405b34f..d8f034b 100644 --- a/pages.go +++ b/pages.go @@ -135,3 +135,30 @@ WriteFreely can communicate with other federated platforms like Mastodon, so peo } return "" } + +func getReaderSection(app *App) (*instanceContent, error) { + c, err := app.db.GetDynamicContent("reader") + if err != nil { + return nil, err + } + if c == nil { + c = &instanceContent{ + ID: "reader", + Type: "section", + Content: defaultReaderBanner(app.cfg), + Updated: defaultPageUpdatedTime, + } + } + if !c.Title.Valid { + c.Title = defaultReaderTitle(app.cfg) + } + return c, nil +} + +func defaultReaderTitle(cfg *config.Config) sql.NullString { + return sql.NullString{String: "Reader", Valid: true} +} + +func defaultReaderBanner(cfg *config.Config) string { + return "Read the latest posts from " + cfg.App.SiteName + "." +} diff --git a/pages/login.tmpl b/pages/login.tmpl index 9b58523..1c8e862 100644 --- a/pages/login.tmpl +++ b/pages/login.tmpl @@ -12,8 +12,8 @@ {{end}}
-
-
+
+
{{if .To}}{{end}}
diff --git a/postrender.go b/postrender.go index af715be..83fb5ad 100644 --- a/postrender.go +++ b/postrender.go @@ -12,17 +12,19 @@ package writefreely import ( "fmt" - "github.com/microcosm-cc/bluemonday" - stripmd "github.com/writeas/go-strip-markdown" - "github.com/writeas/saturday" - "github.com/writeas/web-core/stringmanip" - "github.com/writeas/writefreely/parse" "html" "html/template" "regexp" "strings" "unicode" "unicode/utf8" + + "github.com/microcosm-cc/bluemonday" + stripmd "github.com/writeas/go-strip-markdown" + blackfriday "github.com/writeas/saturday" + "github.com/writeas/web-core/stringmanip" + "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/parse" ) var ( @@ -34,27 +36,28 @@ var ( markeddownReg = regexp.MustCompile("

(.+)

") ) -func (p *Post) formatContent(c *Collection, isOwner bool) { +func (p *Post) formatContent(cfg *config.Config, c *Collection, isOwner bool) { baseURL := c.CanonicalURL() + // TODO: redundant if !isSingleUser { baseURL = "/" + c.Alias + "/" } p.HTMLTitle = template.HTML(applyBasicMarkdown([]byte(p.Title.String))) - p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL)) + p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), baseURL, cfg)) if exc := strings.Index(string(p.Content), ""); exc > -1 { - p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL)) + p.HTMLExcerpt = template.HTML(applyMarkdown([]byte(p.Content[:exc]), baseURL, cfg)) } } -func (p *PublicPost) formatContent(isOwner bool) { - p.Post.formatContent(&p.Collection.Collection, isOwner) +func (p *PublicPost) formatContent(cfg *config.Config, isOwner bool) { + p.Post.formatContent(cfg, &p.Collection.Collection, isOwner) } -func applyMarkdown(data []byte, baseURL string) string { - return applyMarkdownSpecial(data, false, baseURL) +func applyMarkdown(data []byte, baseURL string, cfg *config.Config) string { + return applyMarkdownSpecial(data, false, baseURL, cfg) } -func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string { +func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string, cfg *config.Config) string { mdExtensions := 0 | blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | @@ -74,7 +77,11 @@ func applyMarkdownSpecial(data []byte, skipNoFollow bool, baseURL string) string md := blackfriday.Markdown([]byte(data), blackfriday.HtmlRenderer(htmlFlags, "", ""), mdExtensions) if baseURL != "" { // Replace special text generated by Markdown parser - md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) + tagPrefix := baseURL + "tag:" + if cfg.App.Chorus { + tagPrefix = "/read/t/" + } + md = []byte(hashtagReg.ReplaceAll(md, []byte("#$1"))) } // Strip out bad HTML policy := getSanitizationPolicy() @@ -163,6 +170,7 @@ func getSanitizationPolicy() *bluemonday.Policy { policy.AllowAttrs("controls", "loop", "muted", "autoplay").OnElements("video") policy.AllowAttrs("controls", "loop", "muted", "autoplay", "preload").OnElements("audio") policy.AllowAttrs("target").OnElements("a") + policy.AllowAttrs("title").OnElements("abbr") policy.AllowAttrs("style", "class", "id").Globally() policy.AllowURLSchemes("http", "https", "mailto", "xmpp") return policy diff --git a/posts.go b/posts.go index 2d64808..33d4638 100644 --- a/posts.go +++ b/posts.go @@ -35,6 +35,7 @@ import ( "github.com/writeas/web-core/i18n" "github.com/writeas/web-core/log" "github.com/writeas/web-core/tags" + "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" "github.com/writeas/writefreely/parse" ) @@ -376,7 +377,7 @@ func handleViewPost(app *App, w http.ResponseWriter, r *http.Request) error { Direction: d, } if !isRaw { - post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "")) + post.HTMLContent = template.HTML(applyMarkdown([]byte(content), "", app.cfg)) } } @@ -1088,7 +1089,7 @@ func fetchPost(app *App, w http.ResponseWriter, r *http.Request) error { } p.Collection = &CollectionObj{Collection: *coll} - po := p.ActivityObject() + po := p.ActivityObject(app.cfg) po.Context = []interface{}{activitystreams.Namespace} return impart.RenderActivityJSON(w, po, http.StatusOK) } @@ -1123,7 +1124,7 @@ func (p *PublicPost) CanonicalURL() string { return p.Collection.CanonicalURL() + p.Slug.String } -func (p *PublicPost) ActivityObject() *activitystreams.Object { +func (p *PublicPost) ActivityObject(cfg *config.Config) *activitystreams.Object { o := activitystreams.NewArticleObject() o.ID = p.Collection.FederatedAPIBase() + "api/posts/" + p.ID o.Published = p.Created @@ -1134,7 +1135,7 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object { } o.Name = p.DisplayTitle() if p.HTMLContent == template.HTML("") { - p.formatContent(false) + p.formatContent(cfg, false) } o.Content = string(p.HTMLContent) if p.Language.Valid { @@ -1149,7 +1150,11 @@ func (p *PublicPost) ActivityObject() *activitystreams.Object { if isSingleUser { tagBaseURL = p.Collection.CanonicalURL() + "tag:" } else { - tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) + if cfg.App.Chorus { + tagBaseURL = fmt.Sprintf("%s/read/t/", p.Collection.hostName) + } else { + tagBaseURL = fmt.Sprintf("%s/%s/tag:", p.Collection.hostName, p.Collection.Alias) + } } for _, t := range p.Tags { o.Tag = append(o.Tag, activitystreams.Tag{ @@ -1422,14 +1427,14 @@ Are you sure it was ever here?`, return ErrCollectionPageNotFound } p.extractData() - ap := p.ActivityObject() + ap := p.ActivityObject(app.cfg) ap.Context = []interface{}{activitystreams.Namespace} return impart.RenderActivityJSON(w, ap, http.StatusOK) } else { p.extractData() p.Content = strings.Replace(p.Content, "", "", 1) // TODO: move this to function - p.formatContent(cr.isCollOwner) + p.formatContent(app.cfg, cr.isCollOwner) tp := struct { *PublicPost page.StaticPage @@ -1438,6 +1443,8 @@ Are you sure it was ever here?`, IsCustomDomain bool PinnedPosts *[]PublicPost IsFound bool + IsAdmin bool + CanInvite bool }{ PublicPost: p, StaticPage: pageForReq(app, r), @@ -1445,13 +1452,19 @@ Are you sure it was ever here?`, IsCustomDomain: cr.isCustomDomain, IsFound: postFound, } - tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll) + tp.IsAdmin = u != nil && u.IsAdmin() + tp.CanInvite = canUserInvite(app.cfg, tp.IsAdmin) + tp.PinnedPosts, _ = app.db.GetPinnedPosts(coll, p.IsOwner) tp.IsPinned = len(*tp.PinnedPosts) > 0 && PostsContains(tp.PinnedPosts, p) if !postFound { w.WriteHeader(http.StatusNotFound) } - if err := templates["collection-post"].ExecuteTemplate(w, "post", tp); err != nil { + postTmpl := "collection-post" + if app.cfg.App.Chorus { + postTmpl = "chorus-collection-post" + } + if err := templates[postTmpl].ExecuteTemplate(w, "post", tp); err != nil { log.Error("Error in collection-post template: %v", err) } } diff --git a/read.go b/read.go index e7d1e55..86664b5 100644 --- a/read.go +++ b/read.go @@ -48,6 +48,13 @@ type readPublication struct { Posts *[]PublicPost CurrentPage int TotalPages int + SelTopic string + IsAdmin bool + CanInvite bool + + // Customizable page content + ContentTitle string + Content template.HTML } func initLocalTimeline(app *App) { @@ -99,7 +106,7 @@ func (app *App) FetchPublicPosts() (interface{}, error) { } p.extractData() - p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "")) + p.HTMLContent = template.HTML(applyMarkdown([]byte(p.Content), "", app.cfg)) fp := p.processPost() if isCollectionPost { fp.Collection = &CollectionObj{Collection: *c} @@ -199,13 +206,25 @@ func showLocalTimeline(app *App, w http.ResponseWriter, r *http.Request, page in } d := &readPublication{ - pageForReq(app, r), - &posts, - page, - ttlPages, + StaticPage: pageForReq(app, r), + Posts: &posts, + CurrentPage: page, + TotalPages: ttlPages, + SelTopic: tag, + } + if app.cfg.App.Chorus { + u := getUserSession(app, r) + d.IsAdmin = u != nil && u.IsAdmin() + d.CanInvite = canUserInvite(app.cfg, d.IsAdmin) + } + c, err := getReaderSection(app) + if err != nil { + return err } + d.ContentTitle = c.Title.String + d.Content = template.HTML(applyMarkdown([]byte(c.Content), "", app.cfg)) - err := templates["read"].ExecuteTemplate(w, "base", d) + err = templates["read"].ExecuteTemplate(w, "base", d) if err != nil { log.Error("Unable to render reader: %v", err) fmt.Fprintf(w, ":(") @@ -288,7 +307,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e Title: title, Link: &Link{Href: permalink}, Description: "", - Content: applyMarkdown([]byte(p.Content), ""), + Content: applyMarkdown([]byte(p.Content), "", app.cfg), Author: &Author{author, ""}, Created: p.Created, Updated: p.Updated, diff --git a/routes.go b/routes.go index e7014cd..510a539 100644 --- a/routes.go +++ b/routes.go @@ -152,7 +152,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { // Handle special pages first write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) - write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET") + write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired)) + write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelOptional)).Methods("GET") // TODO: show a reader-specific 404 page if the function is disabled write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) RouteRead(handler, UserLevelReader, write.PathPrefix("/read").Subrouter()) diff --git a/sitemap.go b/sitemap.go index 4dfd953..00e148f 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(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 7a45c45..6e9a008 100644 --- a/templates.go +++ b/templates.go @@ -64,11 +64,14 @@ func initTemplate(parentDir, name string) { filepath.Join(parentDir, templatesDir, "include", "footer.tmpl"), filepath.Join(parentDir, templatesDir, "base.tmpl"), } - if name == "collection" || name == "collection-tags" { + if name == "collection" || name == "collection-tags" || name == "chorus-collection" { // 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 == "collection" || name == "collection-tags" || name == "collection-post" || name == "post" { + 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" { 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/bare.tmpl b/templates/bare.tmpl new file mode 100644 index 0000000..a4194c9 --- /dev/null +++ b/templates/bare.tmpl @@ -0,0 +1,235 @@ +{{define "pad"}} + + + + {{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}} + + + + + + + + +
+ + + +
+
+ {{if not .SingleUser}}

{{if .Chorus}}{{else}}{{end}}{{.SiteName}}

{{end}} + + +
+ +
+ {{if .Editing}}{{end}} +
+
+
+ + + + +{{end}} diff --git a/templates/base.tmpl b/templates/base.tmpl index 775dac9..aae7850 100644 --- a/templates/base.tmpl +++ b/templates/base.tmpl @@ -13,14 +13,49 @@
-

{{.SiteName}}

+ {{ if .Chorus }} {{end}}
diff --git a/templates/chorus-collection-post.tmpl b/templates/chorus-collection-post.tmpl new file mode 100644 index 0000000..bab2e31 --- /dev/null +++ b/templates/chorus-collection-post.tmpl @@ -0,0 +1,150 @@ +{{define "post"}} + + + + + {{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}} + + + + + + + + + {{if gt .Views 1}} + {{end}} + + + + + + + {{if gt (len .Images) 0}}{{else}}{{end}} + + + + + + + {{range .Images}}{{else}}{{end}} + + {{if .Collection.StyleSheet}}{{end}} + + + {{if .Collection.RenderMathJax}} + + {{template "mathjax" . }} + {{end}} + + + {{template "highlighting" .}} + + + + +
+ + {{template "user-navigation" .}} + +
{{if .IsScheduled}}

Scheduled

{{end}}{{if .Title.String}}

{{.FormattedDisplayTitle}}

{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}
{{.HTMLContent}}
+ + {{ if .Collection.ShowFooterBranding }} + + {{ end }} + + + {{if .Collection.CanShowScript}} + {{range .Collection.ExternalScripts}}{{end}} + {{if .Collection.Script}}{{end}} + {{end}} + +{{end}} diff --git a/templates/chorus-collection.tmpl b/templates/chorus-collection.tmpl new file mode 100644 index 0000000..e36d3b5 --- /dev/null +++ b/templates/chorus-collection.tmpl @@ -0,0 +1,230 @@ +{{define "collection"}} + + + + + {{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}} + + + + + {{if gt .CurrentPage 1}}{{end}} + {{if lt .CurrentPage .TotalPages}}{{end}} + {{if not .IsPrivate}}{{end}} + + + + + + + + + + + + + + + + + {{if .StyleSheet}}{{end}} + + + {{if .RenderMathJax}} + + {{template "mathjax" .}} + {{end}} + + + {{template "highlighting" . }} + + + + {{template "user-navigation" .}} + +
+

{{.DisplayTitle}}

+ {{if .Description}}

{{.Description}}

{{end}} + {{/*if not .Public/*}} + + {{/*end*/}} + {{if .PinnedPosts}} + {{end}} +
+ + {{if .Posts}}
{{else}}
{{end}} + + {{if .IsWelcome}} +
+

Welcome, {{.Username}}!

+

This is your new blog.

+

Start writing, or customize your blog.

+

Check out our writing guide to see what else you can do, and get in touch anytime with questions or feedback.

+
+ {{end}} + + {{template "posts" .}} + + {{if gt .TotalPages 1}}{{end}} + + {{if .Posts}}
{{else}}{{end}} + + {{if .ShowFooterBranding }} + + {{ end }} + + + {{if .CanShowScript}} + {{range .ExternalScripts}}{{end}} + {{if .Script}}{{end}} + {{end}} + + + +{{end}} diff --git a/templates/collection.tmpl b/templates/collection.tmpl index 6623a2e..36a266b 100644 --- a/templates/collection.tmpl +++ b/templates/collection.tmpl @@ -48,6 +48,7 @@ {{else}}
  • {{.SiteName}}
  • {{end}} + {{if .SimpleNav}}
  • New Post
  • {{end}}
  • Customize
  • Stats

  • diff --git a/templates/read.tmpl b/templates/read.tmpl index cd03b9d..9541ab5 100644 --- a/templates/read.tmpl +++ b/templates/read.tmpl @@ -65,18 +65,23 @@ } body#collection header nav { display: inline !important; + } + body#collection header nav:not(#full-nav):not(#user-nav) { margin: 0 0 0 1em !important; } header nav#user-nav { margin-left: 0 !important; } + body#collection header nav.tabs a:first-child { + margin-left: 1em; + } {{end}} {{define "body-attrs"}}id="collection"{{end}} {{define "content"}}
    -

    Reader

    -

    Read the latest posts from {{.SiteName}}. {{if .Username}}To showcase your writing here, go to your blog settings and select the Public option.{{end}}

    +

    {{.ContentTitle}}

    + {{if .SelTopic}}#{{.SelTopic}} posts{{else}}{{.Content}}{{end}}

    {{ if gt (len .Posts) 0 }} diff --git a/templates/user/admin/pages.tmpl b/templates/user/admin/pages.tmpl index 25f7984..7a9e66a 100644 --- a/templates/user/admin/pages.tmpl +++ b/templates/user/admin/pages.tmpl @@ -20,6 +20,9 @@ table.classy.export .disabled, table.classy.export a { Home + {{if .LocalTimeline}} + Reader + {{end}} {{range .Pages}} {{if .Title.Valid}}{{.Title.String}}{{else}}{{.ID}}{{end}} diff --git a/templates/user/admin/view-page.tmpl b/templates/user/admin/view-page.tmpl index 6d98b9d..161e40b 100644 --- a/templates/user/admin/view-page.tmpl +++ b/templates/user/admin/view-page.tmpl @@ -31,6 +31,8 @@ input[type=text] {

    Describe what your instance is about.

    {{else if eq .Content.ID "privacy"}}

    Outline your privacy policy.

    + {{else if eq .Content.ID "reader"}} +

    Customize your Reader page.

    {{else if eq .Content.ID "landing"}}

    Customize your home page.

    {{end}} @@ -38,7 +40,7 @@ input[type=text] { {{if .Message}}

    {{.Message}}

    {{end}}
    - {{if eq .Content.Type "section"}} + {{if .Banner}} diff --git a/templates/user/collections.tmpl b/templates/user/collections.tmpl index 6ce4b75..481fd8f 100644 --- a/templates/user/collections.tmpl +++ b/templates/user/collections.tmpl @@ -13,7 +13,7 @@ {{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}

    - new post + new post customize stats

    diff --git a/templates/user/include/header.tmpl b/templates/user/include/header.tmpl index 312d0b8..0feca89 100644 --- a/templates/user/include/header.tmpl +++ b/templates/user/include/header.tmpl @@ -1,21 +1,5 @@ -{{define "header"}} - - - - - {{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}} - - - - - - - - - - - - +{{define "user-navigation"}} +
    {{if .SingleUser}} {{else}} -

    {{.SiteName}}

    + {{ if .Chorus }} + {{end}} {{end}}
    +{{end}} +{{define "header"}} + + + + + {{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}} + + + + + + + + + + + + {{template "user-navigation" .}}
    {{end}} diff --git a/templates/user/invite-help.tmpl b/templates/user/invite-help.tmpl new file mode 100644 index 0000000..978cfad --- /dev/null +++ b/templates/user/invite-help.tmpl @@ -0,0 +1,32 @@ +{{define "invite-help"}} +{{template "header" .}} + +
    +

    Invite to {{.SiteName}}

    + {{ if .Expired }} +

    This invite link is expired.

    + {{ else }} +

    Copy the link below and send it to anyone that you want to join {{ .SiteName }}. You could paste it into an email, instant message, text message, or write it down on paper. Anyone who navigates to this special page will be able to create an account.

    + +

    + {{ if gt .Invite.MaxUses.Int64 0 }} + {{if eq .Invite.MaxUses.Int64 1}}Only one user{{else}}Up to {{.Invite.MaxUses.Int64}} users{{end}} can sign up with this link. + {{if gt .Invite.Uses 0}}So far, {{.Invite.Uses}} {{pluralize "person has" "people have" .Invite.Uses}} used it.{{end}} + {{if .Invite.Expires}}It expires on {{.Invite.ExpiresFriendly}}.{{end}} + {{ else }} + It can be used as many times as you like{{if .Invite.Expires}} before {{.Invite.ExpiresFriendly}}, when it expires{{end}}. + {{ end }} +

    + {{ end }} +
    + +{{template "footer" .}} +{{end}} diff --git a/unregisteredusers.go b/unregisteredusers.go index 91daba5..550c83b 100644 --- a/unregisteredusers.go +++ b/unregisteredusers.go @@ -47,6 +47,9 @@ func handleWebSignup(app *App, w http.ResponseWriter, r *http.Request) error { ur.Normalize = true to := "/" + if app.cfg.App.SimpleNav { + to = "/new" + } if ur.InviteCode != "" { to = "/invite/" + ur.InviteCode }