206 lines
6.2 KiB
Go
206 lines
6.2 KiB
Go
package website
|
|
|
|
import (
|
|
"math"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.handmade.network/hmn/hmn/src/db"
|
|
"git.handmade.network/hmn/hmn/src/hmnurl"
|
|
"git.handmade.network/hmn/hmn/src/models"
|
|
"git.handmade.network/hmn/hmn/src/oops"
|
|
"git.handmade.network/hmn/hmn/src/templates"
|
|
"git.handmade.network/hmn/hmn/src/utils"
|
|
)
|
|
|
|
type FeedData struct {
|
|
templates.BaseData
|
|
|
|
Posts []templates.PostListItem
|
|
Pagination templates.Pagination
|
|
}
|
|
|
|
func Feed(c *RequestContext) ResponseData {
|
|
const postsPerPage = 30
|
|
|
|
c.Perf.StartBlock("SQL", "Count posts")
|
|
numPosts, err := db.QueryInt(c.Context(), c.Conn,
|
|
`
|
|
SELECT COUNT(*)
|
|
FROM
|
|
handmade_post AS post
|
|
WHERE
|
|
post.category_kind = ANY ($1)
|
|
AND deleted = FALSE
|
|
AND post.thread_id IS NOT NULL
|
|
`,
|
|
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
|
)
|
|
c.Perf.EndBlock()
|
|
if err != nil {
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts"))
|
|
}
|
|
|
|
numPages := int(math.Ceil(float64(numPosts) / postsPerPage))
|
|
|
|
page := 1
|
|
pageString, hasPage := c.PathParams["page"]
|
|
if hasPage && pageString != "" {
|
|
if pageParsed, err := strconv.Atoi(pageString); err == nil {
|
|
page = pageParsed
|
|
} else {
|
|
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
|
|
}
|
|
}
|
|
if page < 1 || numPages < page {
|
|
return c.Redirect(hmnurl.BuildFeedWithPage(utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
|
|
}
|
|
|
|
howManyPostsToSkip := (page - 1) * postsPerPage
|
|
|
|
pagination := templates.Pagination{
|
|
Current: page,
|
|
Total: numPages,
|
|
|
|
FirstUrl: hmnurl.BuildFeed(),
|
|
LastUrl: hmnurl.BuildFeedWithPage(numPages),
|
|
NextUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page+1, numPages)),
|
|
PreviousUrl: hmnurl.BuildFeedWithPage(utils.IntClamp(1, page-1, numPages)),
|
|
}
|
|
|
|
var currentUserId *int
|
|
if c.CurrentUser != nil {
|
|
currentUserId = &c.CurrentUser.ID
|
|
}
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch posts")
|
|
type feedPostQuery struct {
|
|
Post models.Post `db:"post"`
|
|
Thread models.Thread `db:"thread"`
|
|
Cat models.Category `db:"cat"`
|
|
Proj models.Project `db:"proj"`
|
|
User models.User `db:"auth_user"`
|
|
ThreadLastReadTime *time.Time `db:"tlri.lastread"`
|
|
CatLastReadTime *time.Time `db:"clri.lastread"`
|
|
}
|
|
posts, err := db.Query(c.Context(), c.Conn, feedPostQuery{},
|
|
`
|
|
SELECT $columns
|
|
FROM
|
|
handmade_post AS post
|
|
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
|
|
LEFT OUTER JOIN handmade_threadlastreadinfo AS tlri ON (
|
|
tlri.thread_id = post.thread_id
|
|
AND tlri.user_id = $1
|
|
)
|
|
LEFT OUTER JOIN handmade_categorylastreadinfo AS clri ON (
|
|
clri.category_id = post.category_id
|
|
AND clri.user_id = $1
|
|
)
|
|
LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
|
|
WHERE
|
|
post.category_kind = ANY ($2)
|
|
AND post.deleted = FALSE
|
|
AND post.thread_id IS NOT NULL
|
|
ORDER BY postdate DESC
|
|
LIMIT $3 OFFSET $4
|
|
`,
|
|
currentUserId,
|
|
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
|
|
postsPerPage,
|
|
howManyPostsToSkip,
|
|
)
|
|
c.Perf.EndBlock()
|
|
if err != nil {
|
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
|
|
}
|
|
|
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
|
c.Perf.EndBlock()
|
|
|
|
categoryUrlCache := make(map[int]string)
|
|
getCategoryUrl := func(projectSlug string, cat *models.Category) string {
|
|
_, ok := categoryUrlCache[cat.ID]
|
|
if !ok {
|
|
lineageNames := lineageBuilder.GetLineageSlugs(cat.ID)
|
|
switch cat.Kind {
|
|
case models.CatKindForum:
|
|
categoryUrlCache[cat.ID] = hmnurl.BuildForumCategory(projectSlug, lineageNames[1:], 1)
|
|
// TODO(asaf): Add more kinds!!!
|
|
default:
|
|
categoryUrlCache[cat.ID] = ""
|
|
}
|
|
}
|
|
return categoryUrlCache[cat.ID]
|
|
}
|
|
|
|
c.Perf.StartBlock("FEED", "Build post items")
|
|
var postItems []templates.PostListItem
|
|
for _, iPostResult := range posts.ToSlice() {
|
|
postResult := iPostResult.(*feedPostQuery)
|
|
|
|
hasRead := false
|
|
if postResult.ThreadLastReadTime != nil && postResult.ThreadLastReadTime.After(postResult.Post.PostDate) {
|
|
hasRead = true
|
|
} else if postResult.CatLastReadTime != nil && postResult.CatLastReadTime.After(postResult.Post.PostDate) {
|
|
hasRead = true
|
|
}
|
|
|
|
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
|
|
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
|
Name: postResult.Proj.Name,
|
|
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Slug),
|
|
})
|
|
if postResult.Post.CategoryKind == models.CatKindLibraryResource {
|
|
// TODO(asaf): Fetch library root topic for the project and construct breadcrumb for it
|
|
} else {
|
|
lineage := lineageBuilder.GetLineage(postResult.Cat.ID)
|
|
for i, cat := range lineage {
|
|
name := *cat.Name
|
|
if i == 0 {
|
|
switch cat.Kind {
|
|
case models.CatKindForum:
|
|
name = "Forums"
|
|
case models.CatKindBlog:
|
|
name = "Blog"
|
|
}
|
|
}
|
|
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
|
|
Name: name,
|
|
Url: getCategoryUrl(postResult.Proj.Subdomain(), cat),
|
|
})
|
|
}
|
|
}
|
|
|
|
postItems = append(postItems, templates.PostListItem{
|
|
Title: postResult.Thread.Title,
|
|
Url: hmnurl.BuildForumPost(postResult.Proj.Subdomain(), lineageBuilder.GetLineageSlugs(postResult.Cat.ID)[1:], postResult.Post.ID, postResult.Post.ThreadID),
|
|
User: templates.UserToTemplate(&postResult.User),
|
|
Date: postResult.Post.PostDate,
|
|
Breadcrumbs: breadcrumbs,
|
|
Unread: !hasRead,
|
|
Classes: "post-bg-alternate", // TODO: Should this be the default, and the home page can suppress it?
|
|
Content: postResult.Post.Preview,
|
|
})
|
|
}
|
|
c.Perf.EndBlock()
|
|
|
|
baseData := getBaseData(c)
|
|
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
|
|
|
|
var res ResponseData
|
|
res.WriteTemplate("feed.html", FeedData{
|
|
BaseData: baseData,
|
|
|
|
Posts: postItems,
|
|
Pagination: pagination,
|
|
}, c.Perf)
|
|
|
|
return res
|
|
}
|