hmn/src/website/feed.go

206 lines
6.2 KiB
Go
Raw Normal View History

2021-04-25 19:33:22 +00:00
package website
import (
"math"
"net/http"
"strconv"
"time"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/hmnurl"
2021-04-25 19:33:22 +00:00
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/templates"
2021-05-05 20:34:32 +00:00
"git.handmade.network/hmn/hmn/src/utils"
2021-04-25 19:33:22 +00:00
)
type FeedData struct {
templates.BaseData
Posts []templates.PostListItem
Pagination templates.Pagination
}
func Feed(c *RequestContext) ResponseData {
const postsPerPage = 30
2021-04-26 06:56:49 +00:00
c.Perf.StartBlock("SQL", "Count posts")
2021-04-25 19:33:22 +00:00
numPosts, err := db.QueryInt(c.Context(), c.Conn,
`
SELECT COUNT(*)
FROM
handmade_post AS post
WHERE
post.category_kind = ANY ($1)
2021-05-05 20:34:32 +00:00
AND deleted = FALSE
AND post.thread_id IS NOT NULL
2021-04-25 19:33:22 +00:00
`,
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
)
2021-04-26 06:56:49 +00:00
c.Perf.EndBlock()
2021-04-25 19:33:22 +00:00
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to get count of feed posts"))
}
2021-04-29 04:52:27 +00:00
numPages := int(math.Ceil(float64(numPosts) / postsPerPage))
2021-04-25 19:33:22 +00:00
page := 1
2021-04-29 03:07:14 +00:00
pageString, hasPage := c.PathParams["page"]
if hasPage && pageString != "" {
2021-04-25 19:33:22 +00:00
if pageParsed, err := strconv.Atoi(pageString); err == nil {
page = pageParsed
} else {
2021-05-05 20:34:32 +00:00
return c.Redirect(hmnurl.BuildFeed(), http.StatusSeeOther)
2021-04-25 19:33:22 +00:00
}
}
if page < 1 || numPages < page {
2021-05-05 20:34:32 +00:00
return c.Redirect(hmnurl.BuildFeedWithPage(utils.IntClamp(1, page, numPages)), http.StatusSeeOther)
2021-04-25 19:33:22 +00:00
}
howManyPostsToSkip := (page - 1) * postsPerPage
pagination := templates.Pagination{
Current: page,
Total: numPages,
2021-05-05 20:34:32 +00:00
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)),
2021-04-25 19:33:22 +00:00
}
var currentUserId *int
if c.CurrentUser != nil {
currentUserId = &c.CurrentUser.ID
}
2021-04-26 06:56:49 +00:00
c.Perf.StartBlock("SQL", "Fetch posts")
2021-04-25 19:33:22 +00:00
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"`
2021-04-25 19:33:22 +00:00
}
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
2021-04-27 01:49:46 +00:00
JOIN handmade_project AS proj ON proj.id = post.project_id
2021-04-25 19:33:22 +00:00
LEFT OUTER JOIN handmade_threadlastreadinfo AS tlri ON (
2021-04-27 01:49:46 +00:00
tlri.thread_id = post.thread_id
2021-04-25 19:33:22 +00:00
AND tlri.user_id = $1
)
LEFT OUTER JOIN handmade_categorylastreadinfo AS clri ON (
2021-04-27 01:49:46 +00:00
clri.category_id = post.category_id
2021-04-25 19:33:22 +00:00
AND clri.user_id = $1
)
LEFT OUTER JOIN auth_user ON post.author_id = auth_user.id
WHERE
post.category_kind = ANY ($2)
2021-04-29 04:52:27 +00:00
AND post.deleted = FALSE
2021-04-25 19:33:22 +00:00
AND post.thread_id IS NOT NULL
ORDER BY postdate DESC
LIMIT $3 OFFSET $4
2021-04-25 19:33:22 +00:00
`,
currentUserId,
[]models.CategoryKind{models.CatKindForum, models.CatKindBlog, models.CatKindWiki, models.CatKindLibraryResource},
2021-04-25 19:33:22 +00:00
postsPerPage,
howManyPostsToSkip,
)
2021-04-26 06:56:49 +00:00
c.Perf.EndBlock()
2021-04-25 19:33:22 +00:00
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch feed posts"))
}
2021-05-05 20:34:32 +00:00
c.Perf.StartBlock("SQL", "Fetch category tree")
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
c.Perf.EndBlock()
2021-05-05 20:34:32 +00:00
categoryUrlCache := make(map[int]string)
2021-05-06 04:04:58 +00:00
getCategoryUrl := func(projectSlug string, cat *models.Category) string {
2021-05-05 20:34:32 +00:00
_, ok := categoryUrlCache[cat.ID]
if !ok {
lineageNames := lineageBuilder.GetLineageSlugs(cat.ID)
switch cat.Kind {
case models.CatKindForum:
2021-05-06 04:04:58 +00:00
categoryUrlCache[cat.ID] = hmnurl.BuildForumCategory(projectSlug, lineageNames[1:], 1)
2021-05-05 20:34:32 +00:00
// TODO(asaf): Add more kinds!!!
default:
categoryUrlCache[cat.ID] = ""
}
}
return categoryUrlCache[cat.ID]
}
c.Perf.StartBlock("FEED", "Build post items")
2021-04-25 19:33:22 +00:00
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
}
2021-05-05 20:34:32 +00:00
breadcrumbs := make([]templates.Breadcrumb, 0, len(lineageBuilder.GetLineage(postResult.Cat.ID)))
2021-04-25 19:33:22 +00:00
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
2021-05-06 04:04:58 +00:00
Name: postResult.Proj.Name,
Url: hmnurl.ProjectUrl("/", nil, postResult.Proj.Slug),
2021-04-25 19:33:22 +00:00
})
2021-05-05 20:34:32 +00:00
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"
}
}
2021-05-05 20:34:32 +00:00
breadcrumbs = append(breadcrumbs, templates.Breadcrumb{
Name: name,
Url: getCategoryUrl(postResult.Proj.Subdomain(), cat),
})
}
2021-04-25 19:33:22 +00:00
}
postItems = append(postItems, templates.PostListItem{
Title: postResult.Thread.Title,
2021-05-05 20:34:32 +00:00
Url: hmnurl.BuildForumPost(postResult.Proj.Subdomain(), lineageBuilder.GetLineageSlugs(postResult.Cat.ID)[1:], postResult.Post.ID, postResult.Post.ThreadID),
2021-04-25 19:33:22 +00:00
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,
})
}
2021-05-05 20:34:32 +00:00
c.Perf.EndBlock()
2021-04-25 19:33:22 +00:00
baseData := getBaseData(c)
baseData.BodyClasses = append(baseData.BodyClasses, "feed")
var res ResponseData
res.WriteTemplate("feed.html", FeedData{
BaseData: baseData,
Posts: postItems,
Pagination: pagination,
2021-04-26 06:56:49 +00:00
}, c.Perf)
2021-04-25 19:33:22 +00:00
return res
}