Reader-first multi-user instances Resolves T680 T681 T684pull/184/head
@@ -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"), | |||
@@ -129,10 +129,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) | |||
} | |||
@@ -524,7 +524,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 | |||
@@ -570,7 +570,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 | |||
@@ -320,6 +320,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) | |||
} | |||
@@ -343,7 +345,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."} | |||
} | |||
@@ -357,6 +359,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") | |||
@@ -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 | |||
@@ -512,7 +512,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 | |||
} | |||
@@ -541,6 +541,8 @@ type CollectionPage struct { | |||
Username string | |||
Collections *[]Collection | |||
PinnedPosts *[]PublicPost | |||
IsAdmin bool | |||
CanInvite bool | |||
} | |||
func (c *CollectionObj) ScriptDisplay() template.JS { | |||
@@ -746,7 +748,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{ | |||
@@ -755,6 +757,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 | |||
@@ -787,7 +791,11 @@ func handleViewCollection(app *App, w http.ResponseWriter, r *http.Request) erro | |||
// TODO: fix this mess of collections inside collections | |||
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) | |||
} | |||
@@ -836,7 +844,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 | |||
} | |||
@@ -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"` | |||
@@ -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) | |||
@@ -1070,7 +1070,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() | |||
@@ -1115,7 +1115,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()) | |||
} | |||
@@ -1131,7 +1131,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() | |||
@@ -1179,7 +1179,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()) | |||
} | |||
@@ -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) | |||
} | |||
@@ -55,9 +55,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 := "" | |||
@@ -94,7 +94,7 @@ func ViewFeed(app *App, w http.ResponseWriter, req *http.Request) error { | |||
Title: title, | |||
Link: &Link{Href: permalink}, | |||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>", | |||
Content: applyMarkdown([]byte(p.Content), ""), | |||
Content: applyMarkdown([]byte(p.Content), "", app.cfg), | |||
Author: &Author{author, ""}, | |||
Created: p.Created, | |||
Updated: p.Updated, | |||
@@ -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 { | |||
@@ -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; | |||
} | |||
} | |||
@@ -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; | |||
} | |||
} | |||
} | |||
} | |||
@@ -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 | |||
@@ -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 + "." | |||
} |
@@ -12,8 +12,8 @@ | |||
</ul>{{end}} | |||
<form action="/auth/login" method="post" style="text-align: center;margin-top:1em;" onsubmit="disableSubmit()"> | |||
<input type="text" name="alias" placeholder="Username" value="{{.Username}}" {{if not .Username}}autofocus{{end}} /><br /> | |||
<input type="password" name="pass" placeholder="Password" {{if .Username}}autofocus{{end}} /><br /> | |||
<input type="text" name="alias" placeholder="Username" value="{{.LoginUsername}}" {{if not .LoginUsername}}autofocus{{end}} /><br /> | |||
<input type="password" name="pass" placeholder="Password" {{if .LoginUsername}}autofocus{{end}} /><br /> | |||
{{if .To}}<input type="hidden" name="to" value="{{.To}}" />{{end}} | |||
<input type="submit" id="btn-login" value="Login" /> | |||
</form> | |||
@@ -23,6 +23,7 @@ import ( | |||
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" | |||
) | |||
@@ -35,28 +36,28 @@ var ( | |||
markeddownReg = regexp.MustCompile("<p>(.+)</p>") | |||
) | |||
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), "<!--more-->"); 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 | | |||
@@ -76,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("<a href=\""+baseURL+"tag:$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>"))) | |||
tagPrefix := baseURL + "tag:" | |||
if cfg.App.Chorus { | |||
tagPrefix = "/read/t/" | |||
} | |||
md = []byte(hashtagReg.ReplaceAll(md, []byte("<a href=\""+tagPrefix+"$1\" class=\"hashtag\"><span>#</span><span class=\"p-category\">$1</span></a>"))) | |||
} | |||
// Strip out bad HTML | |||
policy := getSanitizationPolicy() | |||
@@ -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)) | |||
} | |||
} | |||
@@ -1032,7 +1033,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) | |||
} | |||
@@ -1067,7 +1068,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 | |||
@@ -1078,7 +1079,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 { | |||
@@ -1093,7 +1094,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{ | |||
@@ -1357,14 +1362,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, "<!--more-->", "", 1) | |||
// TODO: move this to function | |||
p.formatContent(cr.isCollOwner) | |||
p.formatContent(app.cfg, cr.isCollOwner) | |||
tp := struct { | |||
*PublicPost | |||
page.StaticPage | |||
@@ -1373,6 +1378,8 @@ Are you sure it was ever here?`, | |||
IsCustomDomain bool | |||
PinnedPosts *[]PublicPost | |||
IsFound bool | |||
IsAdmin bool | |||
CanInvite bool | |||
}{ | |||
PublicPost: p, | |||
StaticPage: pageForReq(app, r), | |||
@@ -1380,13 +1387,19 @@ Are you sure it was ever here?`, | |||
IsCustomDomain: cr.isCustomDomain, | |||
IsFound: postFound, | |||
} | |||
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) | |||
} | |||
} | |||
@@ -47,6 +47,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) { | |||
@@ -97,7 +104,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} | |||
@@ -197,13 +204,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, ":(") | |||
@@ -286,7 +305,7 @@ func viewLocalTimelineFeed(app *App, w http.ResponseWriter, req *http.Request) e | |||
Title: title, | |||
Link: &Link{Href: permalink}, | |||
Description: "<![CDATA[" + stripmd.Strip(p.Content) + "]]>", | |||
Content: applyMarkdown([]byte(p.Content), ""), | |||
Content: applyMarkdown([]byte(p.Content), "", app.cfg), | |||
Author: &Author{author, ""}, | |||
Created: p.Created, | |||
Updated: p.Updated, | |||
@@ -150,6 +150,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { | |||
// Handle special pages first | |||
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) | |||
write.HandleFunc("/signup", handler.Web(handleViewLanding, UserLevelNoneRequired)) | |||
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET") | |||
// TODO: show a reader-specific 404 page if the function is disabled | |||
write.HandleFunc("/read", handler.Web(viewLocalTimeline, UserLevelReader)) | |||
@@ -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 | |||
@@ -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...)) | |||
@@ -0,0 +1,235 @@ | |||
{{define "pad"}}<!DOCTYPE HTML> | |||
<html> | |||
<head> | |||
<title>{{if .Editing}}Editing {{if .Post.Title}}{{.Post.Title}}{{else}}{{.Post.Id}}{{end}}{{else}}New Post{{end}} — {{.SiteName}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<meta name="google" value="notranslate"> | |||
</head> | |||
<body id="pad" class="light"> | |||
<div id="overlay"></div> | |||
<textarea id="writer" placeholder="Write..." class="{{.Post.Font}}" autofocus>{{if .Post.Title}}# {{.Post.Title}} | |||
{{end}}{{.Post.Content}}</textarea> | |||
<header id="tools"> | |||
<div id="clip"> | |||
{{if not .SingleUser}}<h1>{{if .Chorus}}<a href="/" title="Home">{{else}}<a href="/me/c/" title="View blogs">{{end}}{{.SiteName}}</a></h1>{{end}} | |||
<nav id="target" {{if .SingleUser}}style="margin-left:0"{{end}}><ul> | |||
<li>{{if .Blogs}}<a href="{{$c := index .Blogs 0}}{{$c.CanonicalURL}}">My Posts</a>{{else}}<a>Draft</a>{{end}}</li> | |||
</ul></nav> | |||
<span id="wc" class="hidden if-room room-4">0 words</span> | |||
</div> | |||
<noscript style="margin-left: 2em;"><strong>NOTE</strong>: for now, you'll need Javascript enabled to post.</noscript> | |||
<div id="belt"> | |||
{{if .Editing}}<div class="tool hidden if-room"><a href="{{if .EditCollection}}{{.EditCollection.CanonicalURL}}{{.Post.Slug}}/edit/meta{{else}}/{{if .SingleUser}}d/{{end}}{{.Post.Id}}/meta{{end}}" title="Edit post metadata" id="edit-meta"><img class="ic-24dp" src="/img/ic_info_dark@2x.png" /></a></div>{{end}} | |||
<div class="tool"><button title="Publish your writing" id="publish" style="font-weight: bold">Post</button></div> | |||
</div> | |||
</header> | |||
<script src="/js/h.js"></script> | |||
<script> | |||
var $writer = H.getEl('writer'); | |||
var $btnPublish = H.getEl('publish'); | |||
var $wc = H.getEl("wc"); | |||
var updateWordCount = function() { | |||
var words = 0; | |||
var val = $writer.el.value.trim(); | |||
if (val != '') { | |||
words = $writer.el.value.trim().replace(/\s+/gi, ' ').split(' ').length; | |||
} | |||
$wc.el.innerText = words + " word" + (words != 1 ? "s" : ""); | |||
}; | |||
var setButtonStates = function() { | |||
if (!canPublish) { | |||
$btnPublish.el.className = 'disabled'; | |||
return; | |||
} | |||
if ($writer.el.value.length === 0 || (draftDoc != 'lastDoc' && $writer.el.value == origDoc)) { | |||
$btnPublish.el.className = 'disabled'; | |||
} else { | |||
$btnPublish.el.className = ''; | |||
} | |||
}; | |||
{{if .Post.Id}}var draftDoc = 'draft{{.Post.Id}}'; | |||
var origDoc = '{{.Post.Content}}';{{else}}var draftDoc = 'lastDoc';{{end}} | |||
H.load($writer, draftDoc, true); | |||
updateWordCount(); | |||
var typingTimer; | |||
var doneTypingInterval = 200; | |||
var posts; | |||
{{if and .Post.Id (not .Post.Slug)}} | |||
var token = null; | |||
var curPostIdx; | |||
posts = JSON.parse(H.get('posts', '[]')); | |||
for (var i=0; i<posts.length; i++) { | |||
if (posts[i].id == "{{.Post.Id}}") { | |||
token = posts[i].token; | |||
break; | |||
} | |||
} | |||
var canPublish = token != null; | |||
{{else}}var canPublish = true;{{end}} | |||
var publishing = false; | |||
var justPublished = false; | |||
var publish = function(content, font) { | |||
{{if and (and .Post.Id (not .Post.Slug)) (not .User)}} | |||
if (!token) { | |||
alert("You don't have permission to update this post."); | |||
return; | |||
} | |||
{{end}} | |||
publishing = true; | |||
$btnPublish.el.textContent = 'Posting...'; | |||
$btnPublish.el.disabled = true; | |||
var http = new XMLHttpRequest(); | |||
var lang = navigator.languages ? navigator.languages[0] : (navigator.language || navigator.userLanguage); | |||
lang = lang.substring(0, 2); | |||
var post = H.getTitleStrict(content); | |||
var params = { | |||
body: post.content, | |||
title: post.title, | |||
font: font, | |||
lang: lang | |||
}; | |||
{{ if .Post.Slug }} | |||
var url = "/api/collections/{{.EditCollection.Alias}}/posts/{{.Post.Id}}"; | |||
{{ else if .Post.Id }} | |||
var url = "/api/posts/{{.Post.Id}}"; | |||
if (typeof token === 'undefined' || !token) { | |||
token = ""; | |||
} | |||
params.token = token; | |||
{{ else }} | |||
var url = "/api/posts"; | |||
var postTarget = '{{if .Blogs}}{{$c := index .Blogs 0}}{{$c.Alias}}{{else}}anonymous{{end}}'; | |||
if (postTarget != 'anonymous') { | |||
url = "/api/collections/" + postTarget + "/posts"; | |||
} | |||
{{ end }} | |||
http.open("POST", url, true); | |||
// Send the proper header information along with the request | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
publishing = false; | |||
if (http.status == 200 || http.status == 201) { | |||
data = JSON.parse(http.responseText); | |||
id = data.data.id; | |||
nextURL = '{{if .SingleUser}}/d{{end}}/'+id; | |||
{{ if not .Post.Id }} | |||
// Post created | |||
if (postTarget != 'anonymous') { | |||
nextURL = {{if not .SingleUser}}'/'+postTarget+{{end}}'/'+data.data.slug; | |||
} | |||
editToken = data.data.token; | |||
{{ if not .User }}if (postTarget == 'anonymous') { | |||
// Save the data | |||
var posts = JSON.parse(H.get('posts', '[]')); | |||
{{if .Post.Id}}var newPost = H.createPost("{{.Post.Id}}", token, content); | |||
for (var i=0; i<posts.length; i++) { | |||
if (posts[i].id == "{{.Post.Id}}") { | |||
posts[i].title = newPost.title; | |||
posts[i].summary = newPost.summary; | |||
break; | |||
} | |||
} | |||
nextURL = "/pad/posts";{{else}}posts.push(H.createPost(id, editToken, content));{{end}} | |||
H.set('posts', JSON.stringify(posts)); | |||
} | |||
{{ end }} | |||
{{ end }} | |||
justPublished = true; | |||
if (draftDoc != 'lastDoc') { | |||
H.remove(draftDoc); | |||
{{if .Editing}}H.remove('draft{{.Post.Id}}font');{{end}} | |||
} else { | |||
H.set(draftDoc, ''); | |||
} | |||
{{if .EditCollection}} | |||
window.location = '{{.EditCollection.CanonicalURL}}{{.Post.Slug}}'; | |||
{{else}} | |||
window.location = nextURL; | |||
{{end}} | |||
} else { | |||
$btnPublish.el.textContent = 'Post'; | |||
alert("Failed to post. Please try again."); | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
}; | |||
setButtonStates(); | |||
$writer.on('keyup input', function() { | |||
setButtonStates(); | |||
clearTimeout(typingTimer); | |||
typingTimer = setTimeout(doneTyping, doneTypingInterval); | |||
}, false); | |||
$writer.on('keydown', function(e) { | |||
clearTimeout(typingTimer); | |||
if (e.keyCode == 13 && (e.metaKey || e.ctrlKey)) { | |||
$btnPublish.el.click(); | |||
} | |||
}); | |||
$btnPublish.on('click', function(e) { | |||
e.preventDefault(); | |||
if (!publishing && $writer.el.value) { | |||
var content = $writer.el.value; | |||
publish(content, selectedFont); | |||
} | |||
}); | |||
WebFontConfig = { | |||
custom: { families: [ 'Lora:400,700:latin' ], urls: [ '/css/fonts.css' ] } | |||
}; | |||
var selectedFont = H.get('{{if .Editing}}draft{{.Post.Id}}font{{else}}padFont{{end}}', '{{.Post.Font}}'); | |||
var doneTyping = function() { | |||
if (draftDoc == 'lastDoc' || $writer.el.value != origDoc) { | |||
H.save($writer, draftDoc); | |||
updateWordCount(); | |||
} | |||
}; | |||
window.addEventListener('beforeunload', function(e) { | |||
if (draftDoc != 'lastDoc' && $writer.el.value == origDoc) { | |||
H.remove(draftDoc); | |||
} else if (!justPublished) { | |||
doneTyping(); | |||
} | |||
}); | |||
try { | |||
(function() { | |||
var wf=document.createElement('script'); | |||
wf.src = '/js/webfont.js'; | |||
wf.type='text/javascript'; | |||
wf.async='true'; | |||
var s=document.getElementsByTagName('script')[0]; | |||
s.parentNode.insertBefore(wf, s); | |||
})(); | |||
} catch (e) { | |||
// whatevs | |||
} | |||
</script> | |||
</body> | |||
</html>{{end}} |
@@ -13,14 +13,49 @@ | |||
<body {{template "body-attrs" .}}> | |||
<div id="overlay"></div> | |||
<header> | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
{{ if .Chorus }}<nav id="full-nav"> | |||
<div class="left-side"> | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
</div> | |||
{{ else }} | |||
<h2><a href="/">{{.SiteName}}</a></h2> | |||
{{ end }} | |||
{{if not .SingleUser}} | |||
<nav id="user-nav"> | |||
{{if and .Chorus .Username}} | |||
<nav class="dropdown-nav"> | |||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> | |||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}} | |||
<li><a href="/me/settings">Account settings</a></li> | |||
<li><a href="/me/export">Export</a></li> | |||
{{if .CanInvite}}<li><a href="/me/invites">Invite people</a></li>{{end}} | |||
<li class="separator"><hr /></li> | |||
<li><a href="/me/logout">Log out</a></li> | |||
</ul></li> | |||
</ul> | |||
</nav> | |||
{{end}} | |||
<nav class="tabs"> | |||
{{ if and .SimpleNav (not .SingleUser) }} | |||
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}} | |||
{{ end }} | |||
<a href="/about"{{if eq .Path "/about"}} class="selected"{{end}}>About</a> | |||
{{if and (and (not .SingleUser) .LocalTimeline) .CanViewReader}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}} | |||
{{if and (not .SingleUser) (not .Username)}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{end}} | |||
{{ if not .SingleUser }} | |||
{{ if .Username }} | |||
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}} | |||
{{if and (and .Chorus (eq .MaxBlogs 1)) .Username}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}} | |||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}} | |||
{{ end }} | |||
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read"{{if eq .Path "/read"}} class="selected"{{end}}>Reader</a>{{end}} | |||
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}} | |||
{{if not .Username}}<a href="/login"{{if eq .Path "/login"}} class="selected"{{end}}>Log in</a>{{else if .SimpleNav}}<a href="/me/logout">Log out</a>{{end}} | |||
{{ end }} | |||
</nav> | |||
{{if .Chorus}}{{if .Username}}<div class="right-side" style="font-size: 0.86em;"> | |||
<a class="simple-btn" href="/new">New Post</a> | |||
</div>{{end}} | |||
</nav> | |||
{{end}} | |||
</nav> | |||
{{end}} | |||
</header> | |||
@@ -0,0 +1,150 @@ | |||
{{define "post"}}<!DOCTYPE HTML> | |||
<html {{if .Language.Valid}}lang="{{.Language.String}}"{{end}} dir="{{.Direction}}"> | |||
<head prefix="og: http://ogp.me/ns# article: http://ogp.me/ns/article#"> | |||
<meta charset="utf-8"> | |||
<title>{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{.Collection.DisplayTitle}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<link rel="shortcut icon" href="/favicon.ico" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<link rel="canonical" href="{{.CanonicalURL}}" /> | |||
<meta name="generator" content="WriteFreely"> | |||
<meta name="title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> | |||
<meta name="description" content="{{.Summary}}"> | |||
{{if gt .Views 1}}<meta name="twitter:label1" value="Views"> | |||
<meta name="twitter:data1" value="{{largeNumFmt .Views}}">{{end}} | |||
<meta name="author" content="{{.Collection.Title}}" /> | |||
<meta itemprop="description" content="{{.Summary}}"> | |||
<meta itemprop="datePublished" content="{{.CreatedDate}}" /> | |||
<meta name="twitter:card" content="summary"> | |||
<meta name="twitter:description" content="{{.Summary}}"> | |||
<meta name="twitter:title" content="{{.PlainDisplayTitle}} {{localhtml "title dash" .Language.String}} {{if .Collection.Title}}{{.Collection.Title}}{{else}}{{.Collection.Alias}}{{end}}"> | |||
{{if gt (len .Images) 0}}<meta name="twitter:image" content="{{index .Images 0}}">{{else}}<meta name="twitter:image" content="{{.Collection.AvatarURL}}">{{end}} | |||
<meta property="og:title" content="{{.PlainDisplayTitle}}" /> | |||
<meta property="og:description" content="{{.Summary}}" /> | |||
<meta property="og:site_name" content="{{.Collection.DisplayTitle}}" /> | |||
<meta property="og:type" content="article" /> | |||
<meta property="og:url" content="{{.CanonicalURL}}" /> | |||
<meta property="og:updated_time" content="{{.Created8601}}" /> | |||
{{range .Images}}<meta property="og:image" content="{{.}}" />{{else}}<meta property="og:image" content="{{.Collection.AvatarURL}}">{{end}} | |||
<meta property="article:published_time" content="{{.Created8601}}"> | |||
{{if .Collection.StyleSheet}}<style type="text/css">{{.Collection.StyleSheetDisplay}}</style>{{end}} | |||
<style type="text/css"> | |||
body footer { | |||
max-width: 40rem; | |||
margin: 0 auto; | |||
} | |||
body#post header { | |||
padding: 1em 1rem; | |||
} | |||
article time.dt-published { | |||
display: block; | |||
color: #666; | |||
} | |||
body#post article h2#title{ | |||
margin-bottom: 0.5em; | |||
} | |||
article time.dt-published { | |||
margin-bottom: 1em; | |||
} | |||
</style> | |||
{{if .Collection.RenderMathJax}} | |||
<!-- Add mathjax logic --> | |||
{{template "mathjax" . }} | |||
{{end}} | |||
<!-- Add highlighting logic --> | |||
{{template "highlighting" .}} | |||
</head> | |||
<body id="post"> | |||
<div id="overlay"></div> | |||
{{template "user-navigation" .}} | |||
<article id="post-body" class="{{.Font}} h-entry">{{if .IsScheduled}}<p class="badge">Scheduled</p>{{end}}{{if .Title.String}}<h2 id="title" class="p-name">{{.FormattedDisplayTitle}}</h2>{{end}}{{/* TODO: check format: if .Collection.Format.ShowDates*/}}<time class="dt-published" datetime="{{.Created}}" pubdate itemprop="datePublished" content="{{.Created}}">{{.DisplayDate}}</time><div class="e-content">{{.HTMLContent}}</div></article> | |||
{{ if .Collection.ShowFooterBranding }} | |||
<footer dir="ltr"> | |||
<p style="text-align: left">Published by <a rel="author" href="{{if .IsTopLevel}}/{{else}}/{{.Collection.Alias}}/{{end}}" class="h-card p-author">{{.Collection.DisplayTitle}}</a> | |||
{{ if .IsOwner }} · <span class="views" dir="ltr"><strong>{{largeNumFmt .Views}}</strong> {{pluralize "view" "views" .Views}}</span> | |||
· <a class="xtra-feature" href="/{{if not .SingleUser}}{{.Collection.Alias}}/{{end}}{{.Slug.String}}/edit" dir="{{.Direction}}">Edit</a> | |||
{{if .IsPinned}} · <a class="xtra-feature unpin" href="/{{.Collection.Alias}}/{{.Slug.String}}/unpin" dir="{{.Direction}}" onclick="unpinPost(event, '{{.ID}}')">Unpin</a>{{end}} | |||
{{ end }} | |||
</p> | |||
<nav> | |||
{{if .PinnedPosts}} | |||
{{range .PinnedPosts}}<a class="pinned{{if eq .Slug.String $.Slug.String}} selected{{end}}" href="{{if not $.SingleUser}}/{{$.Collection.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}} | |||
{{end}} | |||
</nav> | |||
<hr> | |||
<nav><p style="font-size: 0.9em">{{localhtml "published with write.as" .Language.String}}</p></nav> | |||
</footer> | |||
{{ end }} | |||
</body> | |||
{{if .Collection.CanShowScript}} | |||
{{range .Collection.ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} | |||
{{if .Collection.Script}}<script type="text/javascript">{{.Collection.ScriptDisplay}}</script>{{end}} | |||
{{end}} | |||
<script type="text/javascript"> | |||
var pinning = false; | |||
function unpinPost(e, postID) { | |||
e.preventDefault(); | |||
if (pinning) { | |||
return; | |||
} | |||
pinning = true; | |||
var $footer = document.getElementsByTagName('footer')[0]; | |||
var callback = function() { | |||
// Hide current page | |||
var $pinnedNavLink = $footer.getElementsByTagName('nav')[0].querySelector('.pinned.selected'); | |||
$pinnedNavLink.style.display = 'none'; | |||
}; | |||
var $pinBtn = $footer.getElementsByClassName('unpin')[0]; | |||
$pinBtn.innerHTML = '...'; | |||
var http = new XMLHttpRequest(); | |||
var url = "/api/collections/{{.Collection.Alias}}/unpin"; | |||
var params = [ { "id": postID } ]; | |||
http.open("POST", url, true); | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
pinning = false; | |||
if (http.status == 200) { | |||
callback(); | |||
$pinBtn.style.display = 'none'; | |||
$pinBtn.innerHTML = 'Pin'; | |||
} else if (http.status == 409) { | |||
$pinBtn.innerHTML = 'Unpin'; | |||
} else { | |||
$pinBtn.innerHTML = 'Unpin'; | |||
alert("Failed to unpin." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
}; | |||
try { // Fonts | |||
WebFontConfig = { | |||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] } | |||
}; | |||
(function() { | |||
var wf = document.createElement('script'); | |||
wf.src = '/js/webfont.js'; | |||
wf.type = 'text/javascript'; | |||
wf.async = 'true'; | |||
var s = document.getElementsByTagName('script')[0]; | |||
s.parentNode.insertBefore(wf, s); | |||
})(); | |||
} catch (e) { /* ¯\_(ツ)_/¯ */ } | |||
</script> | |||
</html>{{end}} |
@@ -0,0 +1,230 @@ | |||
{{define "collection"}}<!DOCTYPE HTML> | |||
<html {{if .Language}}lang="{{.Language}}"{{end}} dir="{{.Direction}}"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>{{.DisplayTitle}}{{if not .SingleUser}} — {{.SiteName}}{{end}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<link rel="shortcut icon" href="/favicon.ico" /> | |||
<link rel="canonical" href="{{.CanonicalURL}}"> | |||
{{if gt .CurrentPage 1}}<link rel="prev" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} | |||
{{if lt .CurrentPage .TotalPages}}<link rel="next" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{end}} | |||
{{if not .IsPrivate}}<link rel="alternate" type="application/rss+xml" title="{{.DisplayTitle}} » Feed" href="{{.CanonicalURL}}feed/" />{{end}} | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<meta name="generator" content="WriteFreely"> | |||
<meta name="description" content="{{.Description}}"> | |||
<meta itemprop="name" content="{{.DisplayTitle}}"> | |||
<meta itemprop="description" content="{{.Description}}"> | |||
<meta name="twitter:card" content="summary"> | |||
<meta name="twitter:title" content="{{.DisplayTitle}}"> | |||
<meta name="twitter:image" content="{{.AvatarURL}}"> | |||
<meta name="twitter:description" content="{{.Description}}"> | |||
<meta property="og:title" content="{{.DisplayTitle}}" /> | |||
<meta property="og:site_name" content="{{.DisplayTitle}}" /> | |||
<meta property="og:type" content="article" /> | |||
<meta property="og:url" content="{{.CanonicalURL}}" /> | |||
<meta property="og:description" content="{{.Description}}" /> | |||
<meta property="og:image" content="{{.AvatarURL}}"> | |||
{{if .StyleSheet}}<style type="text/css">{{.StyleSheetDisplay}}</style>{{end}} | |||
<style type="text/css"> | |||
body#collection header { | |||
max-width: 40em; | |||
margin: 1em auto; | |||
text-align: left; | |||
padding: 0; | |||
} | |||
body#collection header.multiuser { | |||
max-width: 100%; | |||
margin: 1em; | |||
} | |||
body#collection header nav:not(.pinned-posts) { | |||
display: inline; | |||
} | |||
body#collection header nav.dropdown-nav, | |||
body#collection header nav.tabs, | |||
body#collection header nav.tabs a:first-child { | |||
margin: 0 0 0 1em; | |||
} | |||
</style> | |||
{{if .RenderMathJax}} | |||
<!-- Add mathjax logic --> | |||
{{template "mathjax" .}} | |||
{{end}} | |||
<!-- Add highlighting logic --> | |||
{{template "highlighting" . }} | |||
</head> | |||
<body id="collection" itemscope itemtype="http://schema.org/WebPage"> | |||
{{template "user-navigation" .}} | |||
<header> | |||
<h1 dir="{{.Direction}}" id="blog-title"><a href="/{{if .IsTopLevel}}{{else}}{{.Prefix}}{{.Alias}}/{{end}}" class="h-card p-author u-url" rel="me author">{{.DisplayTitle}}</a></h1> | |||
{{if .Description}}<p class="description p-note">{{.Description}}</p>{{end}} | |||
{{/*if not .Public/*}} | |||
<!--p class="meta-note"><span>Private collection</span>. Only you can see this page.</p--> | |||
{{/*end*/}} | |||
{{if .PinnedPosts}}<nav class="pinned-posts"> | |||
{{range .PinnedPosts}}<a class="pinned" href="{{if not $.SingleUser}}/{{$.Alias}}/{{.Slug.String}}{{else}}{{.CanonicalURL}}{{end}}">{{.PlainDisplayTitle}}</a>{{end}}</nav> | |||
{{end}} | |||
</header> | |||
{{if .Posts}}<section id="wrapper" itemscope itemtype="http://schema.org/Blog">{{else}}<div id="wrapper">{{end}} | |||
{{if .IsWelcome}} | |||
<div id="welcome"> | |||
<h2>Welcome, <strong>{{.Username}}</strong>!</h2> | |||
<p>This is your new blog.</p> | |||
<p><a class="simple-cta" href="/#{{.Alias}}">Start writing</a>, or <a class="simple-cta" href="/me/c/{{.Alias}}">customize</a> your blog.</p> | |||
<p>Check out our <a class="simple-cta" href="https://guides.write.as/writing/?pk_campaign=welcome">writing guide</a> to see what else you can do, and <a class="simple-cta" href="/contact">get in touch</a> anytime with questions or feedback.</p> | |||
</div> | |||
{{end}} | |||
{{template "posts" .}} | |||
{{if gt .TotalPages 1}}<nav id="paging" class="content-container clearfix"> | |||
{{if or (and .Format.Ascending (lt .CurrentPage .TotalPages)) (isRTL .Direction)}} | |||
{{if gt .CurrentPage 1}}<a href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ {{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Previous{{else}}Newer{{end}}</a>{{end}} | |||
{{if lt .CurrentPage .TotalPages}}<a style="float:right;" href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">{{if and .Format.Ascending (lt .CurrentPage .TotalPages)}}Next{{else}}Older{{end}} ⇢</a>{{end}} | |||
{{else}} | |||
{{if lt .CurrentPage .TotalPages}}<a href="{{.NextPageURL .Prefix .CurrentPage .IsTopLevel}}">⇠ Older</a>{{end}} | |||
{{if gt .CurrentPage 1}}<a style="float:right;" href="{{.PrevPageURL .Prefix .CurrentPage .IsTopLevel}}">Newer ⇢</a>{{end}} | |||
{{end}} | |||
</nav>{{end}} | |||
{{if .Posts}}</section>{{else}}</div>{{end}} | |||
{{if .ShowFooterBranding }} | |||
<footer> | |||
<hr /> | |||
<nav dir="ltr"> | |||
{{if not .SingleUser}}<a class="home pubd" href="/">{{.SiteName}}</a> · {{end}}powered by <a style="margin-left:0" href="https://writefreely.org">writefreely</a> | |||
</nav> | |||
</footer> | |||
{{ end }} | |||
</body> | |||
{{if .CanShowScript}} | |||
{{range .ExternalScripts}}<script type="text/javascript" src="{{.}}" async></script>{{end}} | |||
{{if .Script}}<script type="text/javascript">{{.ScriptDisplay}}</script>{{end}} | |||
{{end}} | |||
<script src="/js/h.js"></script> | |||
<script src="/js/postactions.js"></script> | |||
<script type="text/javascript"> | |||
var deleting = false; | |||
function delPost(e, id, owned) { | |||
e.preventDefault(); | |||
if (deleting) { | |||
return; | |||
} | |||
// TODO: UNDO! | |||
if (window.confirm('Are you sure you want to delete this post?')) { | |||
// AJAX | |||
deletePost(id, "", function() { | |||
// Remove post from list | |||
var $postEl = document.getElementById('post-' + id); | |||
$postEl.parentNode.removeChild($postEl); | |||
// TODO: add next post from this collection at the bottom | |||
}); | |||
} | |||
} | |||
var deletePost = function(postID, token, callback) { | |||
deleting = true; | |||
var $delBtn = document.getElementById('post-' + postID).getElementsByClassName('delete action')[0]; | |||
$delBtn.innerHTML = '...'; | |||
var http = new XMLHttpRequest(); | |||
var url = "/api/posts/" + postID; | |||
http.open("DELETE", url, true); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
deleting = false; | |||
if (http.status == 204) { | |||
callback(); | |||
} else if (http.status == 409) { | |||
$delBtn.innerHTML = 'delete'; | |||
alert("Post is synced to another account. Delete the post from that account instead."); | |||
// TODO: show "remove" button instead of "delete" now | |||
// Persist that state. | |||
// Have it remove the post locally only. | |||
} else { | |||
$delBtn.innerHTML = 'delete'; | |||
alert("Failed to delete." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(); | |||
}; | |||
var pinning = false; | |||
function pinPost(e, postID, slug, title) { | |||
e.preventDefault(); | |||
if (pinning) { | |||
return; | |||
} | |||
pinning = true; | |||
var callback = function() { | |||
// Visibly remove post from collection | |||
var $postEl = document.getElementById('post-' + postID); | |||
$postEl.parentNode.removeChild($postEl); | |||
var $header = document.querySelector('header:not(.multiuser)'); | |||
var $pinnedNavs = $header.getElementsByTagName('nav'); | |||
// Add link to nav | |||
var link = '<a class="pinned" href="/{{.Alias}}/'+slug+'">'+title+'</a>'; | |||
if ($pinnedNavs.length == 0) { | |||
$header.insertAdjacentHTML("beforeend", '<nav>'+link+'</nav>'); | |||
} else { | |||
$pinnedNavs[0].insertAdjacentHTML("beforeend", link); | |||
} | |||
}; | |||
var $pinBtn = document.getElementById('post-' + postID).getElementsByClassName('pin action')[0]; | |||
$pinBtn.innerHTML = '...'; | |||
var http = new XMLHttpRequest(); | |||
var url = "/api/collections/{{.Alias}}/pin"; | |||
var params = [ { "id": postID } ]; | |||
http.open("POST", url, true); | |||
http.setRequestHeader("Content-type", "application/json"); | |||
http.onreadystatechange = function() { | |||
if (http.readyState == 4) { | |||
pinning = false; | |||
if (http.status == 200) { | |||
callback(); | |||
} else if (http.status == 409) { | |||
$pinBtn.innerHTML = 'pin'; | |||
alert("Post is synced to another account. Delete the post from that account instead."); | |||
// TODO: show "remove" button instead of "delete" now | |||
// Persist that state. | |||
// Have it remove the post locally only. | |||
} else { | |||
$pinBtn.innerHTML = 'pin'; | |||
alert("Failed to pin." + (http.status>=500?" Please try again.":"")); | |||
} | |||
} | |||
} | |||
http.send(JSON.stringify(params)); | |||
}; | |||
try { | |||
WebFontConfig = { | |||
custom: { families: [ 'Lora:400,700:latin', 'Open+Sans:400,700:latin' ], urls: [ '/css/fonts.css' ] } | |||
}; | |||
(function() { | |||
var wf = document.createElement('script'); | |||
wf.src = '/js/webfont.js'; | |||
wf.type = 'text/javascript'; | |||
wf.async = 'true'; | |||
var s = document.getElementsByTagName('script')[0]; | |||
s.parentNode.insertBefore(wf, s); | |||
})(); | |||
} catch (e) {} | |||
</script> | |||
</html>{{end}} |
@@ -48,6 +48,7 @@ | |||
{{else}} | |||
<li><a href="/#{{.Alias}}" class="write">{{.SiteName}}</a></li> | |||
{{end}} | |||
{{if .SimpleNav}}<li><a href="/new#{{.Alias}}">New Post</a></li>{{end}} | |||
<li><a href="/me/c/{{.Alias}}">Customize</a></li> | |||
<li><a href="/me/c/{{.Alias}}/stats">Stats</a></li> | |||
<li class="separator"><hr /></li> | |||
@@ -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; | |||
} | |||
</style> | |||
{{end}} | |||
{{define "body-attrs"}}id="collection"{{end}} | |||
{{define "content"}} | |||
<div class="content-container snug" style="max-width: 40rem;"> | |||
<h1 style="text-align:center">Reader</h1> | |||
<p>Read the latest posts from {{.SiteName}}. {{if .Username}}To showcase your writing here, go to your <a href="/me/c/">blog</a> settings and select the <em>Public</em> option.{{end}}</p> | |||
<h1>{{.ContentTitle}}</h1> | |||
<p{{if .SelTopic}} style="text-align:center"{{end}}>{{if .SelTopic}}#{{.SelTopic}} posts{{else}}{{.Content}}{{end}}</p> | |||
</div> | |||
<div id="wrapper"> | |||
{{ if gt (len .Posts) 0 }} | |||
@@ -20,6 +20,9 @@ table.classy.export .disabled, table.classy.export a { | |||
<tr> | |||
<td colspan="2"><a href="/admin/page/landing">Home</a></td> | |||
</tr> | |||
{{if .LocalTimeline}}<tr> | |||
<td colspan="2"><a href="/admin/page/reader">Reader</a></td> | |||
</tr>{{end}} | |||
{{range .Pages}} | |||
<tr> | |||
<td><a href="/admin/page/{{.ID}}">{{if .Title.Valid}}{{.Title.String}}{{else}}{{.ID}}{{end}}</a></td> | |||
@@ -31,6 +31,8 @@ input[type=text] { | |||
<p class="page-desc content-desc">Describe what your instance is <a href="/about" target="page">about</a>.</p> | |||
{{else if eq .Content.ID "privacy"}} | |||
<p class="page-desc content-desc">Outline your <a href="/privacy" target="page">privacy policy</a>.</p> | |||
{{else if eq .Content.ID "reader"}} | |||
<p class="page-desc content-desc">Customize your <a href="/read" target="page">Reader</a> page.</p> | |||
{{else if eq .Content.ID "landing"}} | |||
<p class="page-desc content-desc">Customize your <a href="/?landing=1" target="page">home page</a>.</p> | |||
{{end}} | |||
@@ -38,7 +40,7 @@ input[type=text] { | |||
{{if .Message}}<p>{{.Message}}</p>{{end}} | |||
<form method="post" action="/admin/update/{{.Content.ID}}" onsubmit="savePage(this)"> | |||
{{if eq .Content.Type "section"}} | |||
{{if .Banner}} | |||
<label for="banner"> | |||
Banner | |||
</label> | |||
@@ -13,7 +13,7 @@ | |||
<a class="title" href="/{{.Alias}}/">{{if .Title}}{{.Title}}{{else}}{{.Alias}}{{end}}</a> | |||
</h3> | |||
<h4> | |||
<a class="action new-post" href="/#{{.Alias}}">new post</a> | |||
<a class="action new-post" href="{{if $.Chorus}}/new{{else}}/{{end}}#{{.Alias}}">new post</a> | |||
<a class="action" href="/me/c/{{.Alias}}">customize</a> | |||
<a class="action" href="/me/c/{{.Alias}}/stats">stats</a> | |||
</h4> | |||
@@ -1,21 +1,5 @@ | |||
{{define "header"}}<!DOCTYPE HTML> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<link rel="shortcut icon" href="/favicon.ico" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<meta name="theme-color" content="#888888" /> | |||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}"> | |||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png"> | |||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png"> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png"> | |||
</head> | |||
<body id="me"> | |||
<header{{if .SingleUser}} class="singleuser"{{end}}> | |||
{{define "user-navigation"}} | |||
<header class="{{if .SingleUser}}singleuser{{else}}multiuser{{end}}"> | |||
{{if .SingleUser}} | |||
<nav id="user-nav"> | |||
<nav class="dropdown-nav"> | |||
@@ -38,8 +22,15 @@ | |||
</nav> | |||
</nav> | |||
{{else}} | |||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | |||
{{ if .Chorus }}<nav id="full-nav"> | |||
<div class="left-side"> | |||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | |||
</div> | |||
{{ else }} | |||
<h1><a href="/" title="Return to editor">{{.SiteName}}</a></h1> | |||
{{ end }} | |||
<nav id="user-nav"> | |||
{{if .Username}} | |||
<nav class="dropdown-nav"> | |||
<ul><li><a>{{.Username}}</a> <img class="ic-18dp" src="/img/ic_down_arrow_dark@2x.png" /><ul> | |||
{{if .IsAdmin}}<li><a href="/admin">Admin dashboard</a></li>{{end}} | |||
@@ -51,13 +42,55 @@ | |||
</ul></li> | |||
</ul> | |||
</nav> | |||
{{end}} | |||
<nav class="tabs"> | |||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a> | |||
<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a> | |||
{{if .SimpleNav}} | |||
{{ if not .SingleUser }} | |||
{{if and (and .LocalTimeline .CanViewReader) .Chorus}}<a href="/"{{if eq .Path "/"}} class="selected"{{end}}>Home</a>{{end}} | |||
{{ end }} | |||
<a href="/about">About</a> | |||
{{ if not .SingleUser }} | |||
{{ if .Username }} | |||
{{if gt .MaxBlogs 1}}<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a>{{end}} | |||
{{if and .Chorus (eq .MaxBlogs 1)}}<a href="/{{.Username}}/"{{if eq .Path (printf "/%s/" .Username)}} class="selected"{{end}}>My Posts</a>{{end}} | |||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}} | |||
{{ end }} | |||
{{if and (and .LocalTimeline .CanViewReader) (not .Chorus)}}<a href="/read">Reader</a>{{end}} | |||
{{if and (and (and .Chorus .OpenRegistration) (not .Username)) (or (not .Private) (ne .Landing ""))}}<a href="/signup"{{if eq .Path "/signup"}} class="selected"{{end}}>Sign up</a>{{end}} | |||
{{if .Username}}<a href="/me/logout">Log out</a>{{else}}<a href="/login">Log in</a>{{end}} | |||
{{ end }} | |||
{{else}} | |||
<a href="/me/c/"{{if eq .Path "/me/c/"}} class="selected"{{end}}>Blogs</a> | |||
{{if not .DisableDrafts}}<a href="/me/posts/"{{if eq .Path "/me/posts/"}} class="selected"{{end}}>Drafts</a>{{end}} | |||
{{end}} | |||
</nav> | |||
</nav> | |||
{{if .Chorus}}{{if .Username}}<div class="right-side"> | |||
<a class="simple-btn" href="/new">New Post</a> | |||
</div>{{end}} | |||
</nav> | |||
{{end}} | |||
{{end}} | |||
</header> | |||
{{end}} | |||
{{define "header"}}<!DOCTYPE HTML> | |||
<html> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>{{.PageTitle}} {{if .Separator}}{{.Separator}}{{else}}—{{end}} {{.SiteName}}</title> | |||
<link rel="stylesheet" type="text/css" href="/css/write.css" /> | |||
<link rel="shortcut icon" href="/favicon.ico" /> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||
<meta name="theme-color" content="#888888" /> | |||
<meta name="apple-mobile-web-app-title" content="{{.SiteName}}"> | |||
<link rel="apple-touch-icon" sizes="152x152" href="/img/touch-icon-152.png"> | |||
<link rel="apple-touch-icon" sizes="167x167" href="/img/touch-icon-167.png"> | |||
<link rel="apple-touch-icon" sizes="180x180" href="/img/touch-icon-180.png"> | |||
</head> | |||
<body id="me"> | |||
{{template "user-navigation" .}} | |||
<div id="official-writing"> | |||
{{end}} | |||
@@ -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 | |||
} | |||