Added atom feed and a few other modifications
This commit is contained in:
parent
60b5d07d00
commit
5d9b628144
|
@ -18,6 +18,16 @@ type Q struct {
|
|||
Value string
|
||||
}
|
||||
|
||||
func QFromURL(u *url.URL) []Q {
|
||||
var result []Q
|
||||
for key, values := range u.Query() {
|
||||
for _, v := range values {
|
||||
result = append(result, Q{Name: key, Value: v})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var baseUrlParsed url.URL
|
||||
var cacheBust string
|
||||
var isTest bool
|
||||
|
|
|
@ -50,6 +50,8 @@ func TestSiteMap(t *testing.T) {
|
|||
|
||||
func TestAtomFeed(t *testing.T) {
|
||||
AssertRegexMatch(t, BuildAtomFeed(), RegexAtomFeed, nil)
|
||||
AssertRegexMatch(t, BuildAtomFeedForProjects(), RegexAtomFeed, map[string]string{"feedtype": "projects"})
|
||||
AssertRegexMatch(t, BuildAtomFeedForShowcase(), RegexAtomFeed, map[string]string{"feedtype": "showcase"})
|
||||
}
|
||||
|
||||
func TestLoginAction(t *testing.T) {
|
||||
|
|
|
@ -57,13 +57,6 @@ func BuildSiteMap() string {
|
|||
return Url("/sitemap", nil)
|
||||
}
|
||||
|
||||
var RegexAtomFeed = regexp.MustCompile("^/atom$")
|
||||
|
||||
func BuildAtomFeed() string {
|
||||
defer CatchPanic()
|
||||
return Url("/atom", nil)
|
||||
}
|
||||
|
||||
// QUESTION(ben): Can we change these routes?
|
||||
|
||||
var RegexLoginAction = regexp.MustCompile("^/login$")
|
||||
|
@ -176,6 +169,23 @@ func BuildFeedWithPage(page int) string {
|
|||
return Url("/feed/"+strconv.Itoa(page), nil)
|
||||
}
|
||||
|
||||
var RegexAtomFeed = regexp.MustCompile("^/atom(/(?P<feedtype>.+))?$")
|
||||
|
||||
func BuildAtomFeed() string {
|
||||
defer CatchPanic()
|
||||
return Url("/atom", nil)
|
||||
}
|
||||
|
||||
func BuildAtomFeedForProjects() string {
|
||||
defer CatchPanic()
|
||||
return Url("/atom/projects", nil)
|
||||
}
|
||||
|
||||
func BuildAtomFeedForShowcase() string {
|
||||
defer CatchPanic()
|
||||
return Url("/atom/showcase", nil)
|
||||
}
|
||||
|
||||
/*
|
||||
* Podcast
|
||||
*/
|
||||
|
|
|
@ -46,14 +46,16 @@ var ZerologStackMarshaler = func(err error) interface{} {
|
|||
if asOops, ok := err.(*Error); ok {
|
||||
return asOops.Stack
|
||||
}
|
||||
return nil
|
||||
// NOTE(asaf): If we got here, it means zerolog is trying to output a non-oops error.
|
||||
// We remove this call and the zerolog caller from the stack.
|
||||
return Trace()[2:]
|
||||
}
|
||||
|
||||
func New(wrapped error, format string, args ...interface{}) error {
|
||||
return &Error{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Wrapped: wrapped,
|
||||
Stack: Trace(),
|
||||
Stack: Trace()[1:], // NOTE(asaf): Remove the call to New from the stack
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
|||
Blurb: u.Blurb,
|
||||
Signature: u.Signature,
|
||||
AvatarUrl: avatar,
|
||||
ProfileUrl: hmnurl.Url("m/"+u.Username, nil),
|
||||
ProfileUrl: hmnurl.BuildMember(u.Username),
|
||||
|
||||
DarkTheme: u.DarkTheme,
|
||||
Timezone: u.Timezone,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
{{ noescape "<?xml version=\"1.0\" encoding=\"utf-8\"?>" }}
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title type="text">{{ .Title }}</title>
|
||||
<subtitle type="html">{{ .Subtitle }}</subtitle>
|
||||
<link href="{{ .HomepageUrl }}"/>
|
||||
<link rel="self" type="application/atom+xml" href="{{ .AtomFeedUrl }}"/>
|
||||
<link rel="alternate" type="text/html" hreflang="en" href="{{ .FeedUrl }}"/>
|
||||
<rights type="html">{{ .CopyrightStatement }}</rights>
|
||||
<generator uri="{{ .HomepageUrl }}" version="{{ .SiteVersion }}">Handmade Network site engine v{{ .SiteVersion }}</generator>
|
||||
<updated>{{ rfc3339 .Updated }}</updated>
|
||||
<id>{{ .FeedID }}</id>
|
||||
{{ if .Posts }}
|
||||
{{ range .Posts }}
|
||||
<entry>
|
||||
<title>{{ if .PostTypePrefix }}{{ .PostTypePrefix }}: {{ end }}{{ .Title }}</title>
|
||||
<link rel="alternate" type="text/html" href="{{ .Url }}" />
|
||||
<id>{{ .UUID }}</id>
|
||||
<published>{{ rfc3339 .Date }}</published>
|
||||
<updated>{{ rfc3339 .LastEditDate }}</updated>
|
||||
<author>
|
||||
<name>{{ .User.Name }}</name>
|
||||
<uri>{{ .User.ProfileUrl }}</uri>
|
||||
</author>
|
||||
<summary type="html">{{ .Preview }}</summary>
|
||||
</entry>
|
||||
{{ end }}
|
||||
{{ else if .Projects }}
|
||||
{{ range .Projects }}
|
||||
{{ end }}
|
||||
{{ else if .Snippets }}
|
||||
{{ range .Snippets }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</feed>
|
|
@ -20,7 +20,29 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ range .Posts }}
|
||||
{{ template "post_list_item.html" . }}
|
||||
<div class="post-list-item flex items-center ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }} {{ .Classes }}">
|
||||
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="breadcrumbs">
|
||||
{{ range $i, $breadcrumb := .Breadcrumbs }}
|
||||
{{ if gt $i 0 }} » {{ end }}
|
||||
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ if .PostTypePrefix }}{{ .PostTypePrefix }}: {{ end }}{{ .Title }}</a></div>
|
||||
<div class="details">
|
||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||
</div>
|
||||
{{ with .Preview }}
|
||||
<div class="mt1">
|
||||
{{ noescape . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="goto">
|
||||
<a href="{{ .Url }}">»</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="optionbar bottom">
|
||||
<div>
|
||||
|
|
|
@ -13,15 +13,10 @@ It should be called with PostListItem.
|
|||
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="title nowrap truncate"><a href="{{ .Url }}">{{ .Title }}</a></div>
|
||||
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ .Title }}</a></div>
|
||||
<div class="details">
|
||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||
</div>
|
||||
{{ with .Content }}
|
||||
<div class="mt2">
|
||||
{{ . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="goto">
|
||||
<a href="{{ .Url }}">»</a>
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/*
|
||||
{{/* TODO(asaf): Delete this section once we're done with the landing page
|
||||
{% block columns %}
|
||||
{% include "showcase/js_templates.html" %}
|
||||
{% include "timeline/js_templates.html" %}
|
||||
|
@ -268,7 +268,7 @@
|
|||
{{/* Call this template with a LandingPageFeaturedPost. */}}
|
||||
<div class="flex items-start ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }}">
|
||||
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||
<div class="flex-grow-1">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="title mb1"><a href="{{ .Url }}">{{ .Title }}</a></div>
|
||||
<div class="details">
|
||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||
|
@ -277,7 +277,7 @@
|
|||
<div>
|
||||
{{ .Content }}
|
||||
</div>
|
||||
<div class="excerpt-fade absolute w-100 h4 bottom-0"></div>
|
||||
<div class="excerpt-fade absolute w-100 h4 bottom-0 z-999"></div>
|
||||
</div>
|
||||
<div class="mt2">
|
||||
<a href="{{ .Url }}">Read More →</a>
|
||||
|
@ -285,3 +285,25 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ define "post_list_item" }}
|
||||
{{/* Call this template with a PostListItem. */}}
|
||||
<div class="post-list-item flex items-center ph3 pv2 {{ if .Unread }}unread{{ else }}read{{ end }} {{ .Classes }}">
|
||||
<img class="avatar-icon mr2" src="{{ .User.AvatarUrl }}">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="breadcrumbs">
|
||||
{{ range $i, $breadcrumb := .Breadcrumbs }}
|
||||
{{ if gt $i 0 }} » {{ end }}
|
||||
<a href="{{ $breadcrumb.Url }}">{{ $breadcrumb.Name }}</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="title nowrap truncate"><a href="{{ .Url }}" title="{{ .Preview }}">{{ .Title }}</a></div>
|
||||
<div class="details">
|
||||
<a class="user" href="{{ .User.ProfileUrl }}">{{ .User.Name }}</a> — {{ timehtml (relativedate .Date) .Date }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="goto">
|
||||
<a href="{{ .Url }}">»</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -25,11 +24,7 @@ const (
|
|||
var templateFs embed.FS
|
||||
var Templates map[string]*template.Template
|
||||
|
||||
var cachebust string
|
||||
|
||||
func Init() {
|
||||
cachebust = fmt.Sprint(time.Now().Unix())
|
||||
|
||||
Templates = make(map[string]*template.Template)
|
||||
|
||||
files, _ := templateFs.ReadDir("src")
|
||||
|
@ -53,6 +48,16 @@ func Init() {
|
|||
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||
}
|
||||
|
||||
Templates[f.Name()] = t
|
||||
} else if strings.HasSuffix(f.Name(), ".xml") {
|
||||
t := template.New(f.Name())
|
||||
t = t.Funcs(sprig.FuncMap())
|
||||
t = t.Funcs(HMNTemplateFuncs)
|
||||
t, err := t.ParseFS(templateFs, "src/"+f.Name())
|
||||
if err != nil {
|
||||
logging.Fatal().Str("filename", f.Name()).Err(err).Msg("failed to parse template")
|
||||
}
|
||||
|
||||
Templates[f.Name()] = t
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +73,10 @@ func names(ts []*template.Template) []string {
|
|||
|
||||
var HMNTemplateFuncs = template.FuncMap{
|
||||
"absolutedate": func(t time.Time) string {
|
||||
return t.Format("January 2, 2006, 3:04pm")
|
||||
return t.UTC().Format("January 2, 2006, 3:04pm")
|
||||
},
|
||||
"rfc3339": func(t time.Time) string {
|
||||
return t.UTC().Format(time.RFC3339)
|
||||
},
|
||||
"alpha": func(alpha float64, color noire.Color) noire.Color {
|
||||
color.Alpha = alpha
|
||||
|
@ -77,9 +85,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
"brighten": func(amount float64, color noire.Color) noire.Color {
|
||||
return color.Tint(amount)
|
||||
},
|
||||
"cachebust": func() string {
|
||||
return cachebust
|
||||
},
|
||||
"color2css": func(color noire.Color) template.CSS {
|
||||
return template.CSS(color.HTML())
|
||||
},
|
||||
|
@ -96,20 +101,6 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
h, s, _, a := color.HSLA()
|
||||
return noire.NewHSLA(h, s, lightness*100, a)
|
||||
},
|
||||
"projecturl": func(url string, proj interface{}) string {
|
||||
return hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
|
||||
},
|
||||
"projecturlq": func(url string, proj interface{}, query string) string {
|
||||
absUrl := hmnurl.ProjectUrl(url, nil, getProjectSubdomain(proj))
|
||||
return fmt.Sprintf("%s?%s", absUrl, query)
|
||||
},
|
||||
"query": func(args ...string) string {
|
||||
query := url.Values{}
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
query.Set(args[i], args[i+1])
|
||||
}
|
||||
return query.Encode()
|
||||
},
|
||||
"relativedate": func(t time.Time) string {
|
||||
// TODO: Support relative future dates
|
||||
|
||||
|
@ -161,9 +152,12 @@ var HMNTemplateFuncs = template.FuncMap{
|
|||
return hmnurl.BuildTheme(filepath, theme, false)
|
||||
},
|
||||
"timehtml": func(formatted string, t time.Time) template.HTML {
|
||||
iso := t.Format(time.RFC3339)
|
||||
iso := t.UTC().Format(time.RFC3339)
|
||||
return template.HTML(fmt.Sprintf(`<time datetime="%s">%s</time>`, iso, formatted))
|
||||
},
|
||||
"noescape": func(str string) template.HTML {
|
||||
return template.HTML(str)
|
||||
},
|
||||
}
|
||||
|
||||
type ErrInvalidHexColor struct {
|
||||
|
|
|
@ -135,18 +135,37 @@ type BackgroundImage struct {
|
|||
Size string // A valid CSS background-size value
|
||||
}
|
||||
|
||||
type PostType int
|
||||
|
||||
const (
|
||||
PostTypeUnknown PostType = iota
|
||||
PostTypeBlogPost
|
||||
PostTypeBlogComment
|
||||
PostTypeForumThread
|
||||
PostTypeForumReply
|
||||
PostTypeWikiCreate
|
||||
PostTypeWikiTalk
|
||||
PostTypeWikiEdit
|
||||
PostTypeLibraryComment
|
||||
)
|
||||
|
||||
// Data from post_list_item.html
|
||||
type PostListItem struct {
|
||||
Title string
|
||||
Url string
|
||||
UUID string
|
||||
Breadcrumbs []Breadcrumb
|
||||
|
||||
PostType PostType
|
||||
PostTypePrefix string
|
||||
|
||||
User User
|
||||
Date time.Time
|
||||
|
||||
Unread bool
|
||||
Classes string
|
||||
Content string
|
||||
Preview string
|
||||
LastEditDate time.Time
|
||||
}
|
||||
|
||||
// Data from thread_list_item.html
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package website
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
|
@ -76,9 +80,123 @@ func Feed(c *RequestContext) ResponseData {
|
|||
currentUserId = &c.CurrentUser.ID
|
||||
}
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
posts, err := fetchAllPosts(c, lineageBuilder, currentUserId, howManyPostsToSkip, postsPerPage)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||
}
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
||||
|
||||
var res ResponseData
|
||||
res.WriteTemplate("feed.html", FeedData{
|
||||
BaseData: baseData,
|
||||
|
||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
||||
Posts: posts,
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
type FeedType int
|
||||
|
||||
const (
|
||||
FeedTypeAll = iota
|
||||
FeedTypeProjects
|
||||
FeedTypeShowcase
|
||||
)
|
||||
|
||||
// NOTE(asaf): UUID values copied from old website
|
||||
var (
|
||||
FeedIDAll = "urn:uuid:1084fd28-993a-4961-9011-39ddeaeb3711"
|
||||
FeedIDProjects = "urn:uuid:cfad0d50-cbcf-11e7-82d7-db1d52543cc7"
|
||||
FeedIDShowcase = "urn:uuid:37d29027-2892-5a21-b521-951246c7aa46"
|
||||
)
|
||||
|
||||
type AtomFeedData struct {
|
||||
Title string
|
||||
Subtitle string
|
||||
|
||||
HomepageUrl string
|
||||
AtomFeedUrl string
|
||||
FeedUrl string
|
||||
|
||||
CopyrightStatement string
|
||||
SiteVersion string
|
||||
Updated time.Time
|
||||
FeedID string
|
||||
|
||||
FeedType FeedType
|
||||
Posts []templates.PostListItem
|
||||
Projects []int // TODO(asaf): Actually do this
|
||||
Snippets []int // TODO(asaf): Actually do this
|
||||
}
|
||||
|
||||
func AtomFeed(c *RequestContext) ResponseData {
|
||||
itemsPerFeed := 25 // NOTE(asaf): Copied from old website
|
||||
|
||||
feedData := AtomFeedData{
|
||||
HomepageUrl: hmnurl.BuildHomepage(),
|
||||
|
||||
CopyrightStatement: fmt.Sprintf("Copyright (C) 2014-%d Handmade.Network and its contributors", time.Now().Year()),
|
||||
SiteVersion: "2.0",
|
||||
}
|
||||
|
||||
feedType, hasType := c.PathParams["feedtype"]
|
||||
if !hasType || len(feedType) == 0 {
|
||||
feedData.Title = "New Threads, Blog Posts, Replies and Comments | Site-wide | Handmade.Network"
|
||||
feedData.Subtitle = feedData.Title
|
||||
feedData.FeedType = FeedTypeAll
|
||||
feedData.FeedID = FeedIDAll
|
||||
feedData.AtomFeedUrl = hmnurl.BuildAtomFeed()
|
||||
feedData.FeedUrl = hmnurl.BuildFeed()
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
posts, err := fetchAllPosts(c, lineageBuilder, nil, 0, itemsPerFeed)
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||
}
|
||||
feedData.Posts = posts
|
||||
|
||||
updated := time.Now()
|
||||
if len(posts) > 0 {
|
||||
updated = posts[0].Date
|
||||
}
|
||||
feedData.Updated = updated
|
||||
} else {
|
||||
switch strings.ToLower(feedType) {
|
||||
case "projects":
|
||||
// TODO(asaf): Implement this
|
||||
case "showcase":
|
||||
// TODO(asaf): Implement this
|
||||
default:
|
||||
return FourOhFour(c)
|
||||
}
|
||||
}
|
||||
|
||||
var res ResponseData
|
||||
res.WriteTemplate("atom.xml", feedData, c.Perf)
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func fetchAllPosts(c *RequestContext, lineageBuilder *models.CategoryLineageBuilder, currentUserID *int, offset int, limit int) ([]templates.PostListItem, error) {
|
||||
c.Perf.StartBlock("SQL", "Fetch posts")
|
||||
type feedPostQuery struct {
|
||||
Post models.Post `db:"post"`
|
||||
PostVersion models.PostVersion `db:"version"`
|
||||
Thread models.Thread `db:"thread"`
|
||||
Cat models.Category `db:"cat"`
|
||||
Proj models.Project `db:"proj"`
|
||||
|
@ -92,6 +210,7 @@ func Feed(c *RequestContext) ResponseData {
|
|||
SELECT $columns
|
||||
FROM
|
||||
handmade_post AS post
|
||||
JOIN handmade_postversion AS version ON version.id = post.current_id
|
||||
JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||
JOIN handmade_category AS cat ON cat.id = post.category_id
|
||||
JOIN handmade_project AS proj ON proj.id = post.project_id
|
||||
|
@ -112,21 +231,16 @@ func Feed(c *RequestContext) ResponseData {
|
|||
ORDER BY postdate DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
`,
|
||||
currentUserId,
|
||||
currentUserID,
|
||||
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
||||
postsPerPage,
|
||||
howManyPostsToSkip,
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
c.Perf.EndBlock()
|
||||
if err != nil {
|
||||
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||
c.Perf.EndBlock()
|
||||
|
||||
c.Perf.StartBlock("FEED", "Build post items")
|
||||
var postItems []templates.PostListItem
|
||||
for _, iPostResult := range posts.ToSlice() {
|
||||
|
@ -139,7 +253,7 @@ func Feed(c *RequestContext) ResponseData {
|
|||
hasRead = true
|
||||
}
|
||||
|
||||
postItems = append(postItems, MakePostListItem(
|
||||
postItem := MakePostListItem(
|
||||
lineageBuilder,
|
||||
&postResult.Proj,
|
||||
&postResult.Thread,
|
||||
|
@ -149,22 +263,14 @@ func Feed(c *RequestContext) ResponseData {
|
|||
!hasRead,
|
||||
true,
|
||||
c.Theme,
|
||||
))
|
||||
)
|
||||
|
||||
postItem.UUID = uuid.NewSHA1(uuid.NameSpaceURL, []byte(postItem.Url)).URN()
|
||||
postItem.LastEditDate = postResult.PostVersion.EditDate
|
||||
|
||||
postItems = append(postItems, postItem)
|
||||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
baseData := getBaseData(c)
|
||||
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
||||
|
||||
var res ResponseData
|
||||
res.WriteTemplate("feed.html", FeedData{
|
||||
BaseData: baseData,
|
||||
|
||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
||||
Posts: postItems,
|
||||
Pagination: pagination,
|
||||
}, c.Perf)
|
||||
|
||||
return res
|
||||
return postItems, nil
|
||||
}
|
||||
|
|
|
@ -29,6 +29,24 @@ func UrlForGenericPost(post *models.Post, subforums []string, threadTitle string
|
|||
return hmnurl.BuildProjectHomepage(projectSlug)
|
||||
}
|
||||
|
||||
var PostTypeMap = map[models.CategoryKind][]templates.PostType{
|
||||
models.CatKindBlog: []templates.PostType{templates.PostTypeBlogPost, templates.PostTypeBlogComment},
|
||||
models.CatKindForum: []templates.PostType{templates.PostTypeForumThread, templates.PostTypeForumReply},
|
||||
models.CatKindWiki: []templates.PostType{templates.PostTypeWikiCreate, templates.PostTypeWikiTalk},
|
||||
models.CatKindLibraryResource: []templates.PostType{templates.PostTypeLibraryComment, templates.PostTypeLibraryComment},
|
||||
}
|
||||
|
||||
var PostTypePrefix = map[templates.PostType]string{
|
||||
templates.PostTypeBlogPost: "New blog post",
|
||||
templates.PostTypeBlogComment: "Blog comment",
|
||||
templates.PostTypeForumThread: "New forum thread",
|
||||
templates.PostTypeForumReply: "Forum reply",
|
||||
templates.PostTypeWikiCreate: "New wiki page",
|
||||
templates.PostTypeWikiTalk: "Wiki comment",
|
||||
templates.PostTypeWikiEdit: "Wiki edit",
|
||||
templates.PostTypeLibraryComment: "Library comment",
|
||||
}
|
||||
|
||||
// NOTE(asaf): THIS DOESN'T HANDLE WIKI EDIT ITEMS. Wiki edits are PostTextVersions, not Posts.
|
||||
func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *models.Project, thread *models.Thread, post *models.Post, user *models.User, libraryResource *models.LibraryResource, unread bool, includeBreadcrumbs bool, currentTheme string) templates.PostListItem {
|
||||
var result templates.PostListItem
|
||||
|
@ -42,6 +60,19 @@ func MakePostListItem(lineageBuilder *models.CategoryLineageBuilder, project *mo
|
|||
libraryResourceId = libraryResource.ID
|
||||
}
|
||||
result.Url = UrlForGenericPost(post, lineageBuilder.GetSubforumLineageSlugs(post.CategoryID), thread.Title, libraryResourceId, project.Slug)
|
||||
result.Preview = post.Preview
|
||||
|
||||
postType := templates.PostTypeUnknown
|
||||
postTypeOptions, found := PostTypeMap[post.CategoryKind]
|
||||
if found {
|
||||
var hasParent int
|
||||
if post.ParentID != nil {
|
||||
hasParent = 1
|
||||
}
|
||||
postType = postTypeOptions[hasParent]
|
||||
}
|
||||
result.PostType = postType
|
||||
result.PostTypePrefix = PostTypePrefix[result.PostType]
|
||||
|
||||
if includeBreadcrumbs {
|
||||
result.Breadcrumbs = append(result.Breadcrumbs, templates.Breadcrumb{
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"git.handmade.network/hmn/hmn/src/logging"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/perf"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
|
@ -249,7 +250,11 @@ func (rd *ResponseData) WriteTemplate(name string, data interface{}, rp *perf.Re
|
|||
rp.StartBlock("TEMPLATE", name)
|
||||
defer rp.EndBlock()
|
||||
}
|
||||
return templates.Templates[name].Execute(rd, data)
|
||||
template, hasTemplate := templates.Templates[name]
|
||||
if !hasTemplate {
|
||||
panic(oops.New(nil, "Template not found: %s", name))
|
||||
}
|
||||
return template.Execute(rd, data)
|
||||
}
|
||||
|
||||
func ErrorResponse(status int, errs ...error) ResponseData {
|
||||
|
|
|
@ -73,7 +73,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
}
|
||||
|
||||
if !c.CurrentProject.IsHMN() {
|
||||
return c.Redirect(hmnurl.Url(c.URL().String(), nil), http.StatusMovedPermanently)
|
||||
return c.Redirect(hmnurl.Url(c.URL().Path, hmnurl.QFromURL(c.URL())), http.StatusMovedPermanently)
|
||||
}
|
||||
|
||||
return h(c)
|
||||
|
@ -104,6 +104,7 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
|||
staticPages.GET(hmnurl.RegexProjectSubmissionGuidelines, ProjectSubmissionGuidelines)
|
||||
|
||||
mainRoutes.GET(hmnurl.RegexFeed, Feed)
|
||||
mainRoutes.GET(hmnurl.RegexAtomFeed, AtomFeed)
|
||||
|
||||
// TODO(asaf): Trailing slashes break these
|
||||
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||
|
|
Loading…
Reference in New Issue