Merge pull request #157 from writeas/chorus
Reader-first multi-user instances Resolves T680 T681 T684
This commit is contained in:
commit
6b99d75aa9
17
account.go
17
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"),
|
||||
|
@ -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
|
||||
|
7
admin.go
7
admin.go
@ -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")
|
||||
|
27
app.go
27
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
|
||||
|
@ -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"`
|
||||
|
12
database.go
12
database.go
@ -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)
|
||||
}
|
||||
|
6
feed.go
6
feed.go
@ -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
|
||||
|
27
pages.go
27
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 + "."
|
||||
}
|
||||
|
@ -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()
|
||||
|
29
posts.go
29
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
33
read.go
33
read.go
@ -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...))
|
||||
|
235
templates/bare.tmpl
Normal file
235
templates/bare.tmpl
Normal file
@ -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">
|
||||
<nav class="tabs">
|
||||
<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 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 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>
|
||||
|
150
templates/chorus-collection-post.tmpl
Normal file
150
templates/chorus-collection-post.tmpl
Normal file
@ -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}}
|
230
templates/chorus-collection.tmpl
Normal file
230
templates/chorus-collection.tmpl
Normal file
@ -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
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user