-
{{ .Author.Username }} {{/* TODO: Text scale stuff? Seems unnecessary. */}}
+
{{ .Author.Username }}
{{ if .Author.IsStaff }}
diff --git a/src/website/blogs.go b/src/website/blogs.go
new file mode 100644
index 0000000..d609914
--- /dev/null
+++ b/src/website/blogs.go
@@ -0,0 +1,153 @@
+package website
+
+import (
+ "net/http"
+ "strconv"
+
+ "git.handmade.network/hmn/hmn/src/db"
+ "git.handmade.network/hmn/hmn/src/hmnurl"
+ "git.handmade.network/hmn/hmn/src/templates"
+)
+
+func BlogThread(c *RequestContext) ResponseData {
+ type blogPostData struct {
+ templates.BaseData
+ Thread templates.Thread
+ MainPost templates.Post
+ Comments []templates.Post
+ ReplyLink string
+ LoginLink string
+ }
+
+ cd, ok := getCommonBlogData(c)
+ if !ok {
+ return FourOhFour(c)
+ }
+
+ thread, posts := FetchThreadPostsAndStuff(
+ c.Context(),
+ c.Conn,
+ cd.ThreadID,
+ 0, 0,
+ )
+
+ var templatePosts []templates.Post
+ for _, p := range posts {
+ post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
+ post.AddContentVersion(p.CurrentVersion, p.Editor)
+ addBlogUrlsToPost(&post, c.CurrentProject.Slug, p.Thread.ID, p.Post.ID)
+
+ if p.ReplyPost != nil {
+ reply := templates.PostToTemplate(p.ReplyPost, p.ReplyAuthor, c.Theme)
+ addBlogUrlsToPost(&reply, c.CurrentProject.Slug, p.Thread.ID, p.Post.ID)
+ post.ReplyPost = &reply
+ }
+
+ templatePosts = append(templatePosts, post)
+ }
+
+ baseData := getBaseData(c)
+ baseData.Title = thread.Title
+
+ var res ResponseData
+ res.MustWriteTemplate("blog_post.html", blogPostData{
+ BaseData: baseData,
+ Thread: templates.ThreadToTemplate(&thread),
+ MainPost: templatePosts[0],
+ Comments: templatePosts[1:],
+ }, c.Perf)
+ return res
+}
+
+func BlogPostRedirectToThread(c *RequestContext) ResponseData {
+ cd, ok := getCommonBlogData(c)
+ if !ok {
+ return FourOhFour(c)
+ }
+
+ thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
+
+ threadUrl := hmnurl.BuildBlogThread(c.CurrentProject.Slug, cd.ThreadID, thread.Title, 1)
+ return c.Redirect(threadUrl, http.StatusFound)
+}
+
+type commonBlogData struct {
+ c *RequestContext
+
+ ThreadID int
+ PostID int
+}
+
+func getCommonBlogData(c *RequestContext) (commonBlogData, bool) {
+ c.Perf.StartBlock("BLOGS", "Fetch common blog data")
+ defer c.Perf.EndBlock()
+
+ res := commonBlogData{
+ c: c,
+ }
+
+ if threadIdStr, hasThreadId := c.PathParams["threadid"]; hasThreadId {
+ threadId, err := strconv.Atoi(threadIdStr)
+ if err != nil {
+ return commonBlogData{}, false
+ }
+ res.ThreadID = threadId
+
+ c.Perf.StartBlock("SQL", "Verify that the thread exists")
+ threadExists, err := db.QueryBool(c.Context(), c.Conn,
+ `
+ SELECT COUNT(*) > 0
+ FROM handmade_thread
+ WHERE
+ id = $1
+ AND project_id = $2
+ `,
+ res.ThreadID,
+ c.CurrentProject.ID,
+ )
+ c.Perf.EndBlock()
+ if err != nil {
+ panic(err)
+ }
+ if !threadExists {
+ return commonBlogData{}, false
+ }
+ }
+
+ if postIdStr, hasPostId := c.PathParams["postid"]; hasPostId {
+ postId, err := strconv.Atoi(postIdStr)
+ if err != nil {
+ return commonBlogData{}, false
+ }
+ res.PostID = postId
+
+ c.Perf.StartBlock("SQL", "Verify that the post exists")
+ postExists, err := db.QueryBool(c.Context(), c.Conn,
+ `
+ SELECT COUNT(*) > 0
+ FROM handmade_post
+ WHERE
+ id = $1
+ AND thread_id = $2
+ `,
+ res.PostID,
+ res.ThreadID,
+ )
+ c.Perf.EndBlock()
+ if err != nil {
+ panic(err)
+ }
+ if !postExists {
+ return commonBlogData{}, false
+ }
+ }
+
+ return res, true
+}
+
+func addBlogUrlsToPost(p *templates.Post, projectSlug string, threadId int, postId int) {
+ p.Url = hmnurl.BuildBlogPost(projectSlug, threadId, postId)
+ p.DeleteUrl = hmnurl.BuildBlogPostDelete(projectSlug, threadId, postId)
+ p.EditUrl = hmnurl.BuildBlogPostEdit(projectSlug, threadId, postId)
+ p.ReplyUrl = hmnurl.BuildBlogPostReply(projectSlug, threadId, postId)
+}
diff --git a/src/website/forums.go b/src/website/forums.go
index 1a10f9e..6f869d2 100644
--- a/src/website/forums.go
+++ b/src/website/forums.go
@@ -421,7 +421,7 @@ func ForumThread(c *RequestContext) ResponseData {
currentSubforumSlugs := cd.LineageBuilder.GetSubforumLineageSlugs(cd.SubforumID)
- thread := cd.FetchThread(c.Context(), c.Conn)
+ thread := FetchThread(c.Context(), c.Conn, cd.ThreadID)
numPosts, err := db.QueryInt(c.Context(), c.Conn,
`
@@ -452,52 +452,23 @@ func ForumThread(c *RequestContext) ResponseData {
}
c.Perf.StartBlock("SQL", "Fetch posts")
- type postsQueryResult struct {
- Post models.Post `db:"post"`
- Ver models.PostVersion `db:"ver"`
- Author *models.User `db:"author"`
- Editor *models.User `db:"editor"`
-
- ReplyPost *models.Post `db:"reply"`
- ReplyAuthor *models.User `db:"reply_author"`
- }
- itPosts, err := db.Query(c.Context(), c.Conn, postsQueryResult{},
- `
- SELECT $columns
- FROM
- handmade_post AS post
- JOIN handmade_postversion AS ver ON post.current_id = ver.id
- LEFT JOIN auth_user AS author ON post.author_id = author.id
- LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
- LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
- LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
- WHERE
- post.thread_id = $1
- AND NOT post.deleted
- ORDER BY post.postdate
- LIMIT $2 OFFSET $3
- `,
- thread.ID,
- threadViewPostsPerPage,
- (page-1)*threadViewPostsPerPage,
+ _, postsAndStuff := FetchThreadPostsAndStuff(
+ c.Context(),
+ c.Conn,
+ cd.ThreadID,
+ page, threadViewPostsPerPage,
)
c.Perf.EndBlock()
- if err != nil {
- panic(err)
- }
- defer itPosts.Close()
var posts []templates.Post
- for _, irow := range itPosts.ToSlice() {
- row := irow.(*postsQueryResult)
+ for _, p := range postsAndStuff {
+ post := templates.PostToTemplate(&p.Post, p.Author, c.Theme)
+ post.AddContentVersion(p.CurrentVersion, p.Editor)
+ addForumUrlsToPost(&post, c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
- post := templates.PostToTemplate(&row.Post, row.Author, c.Theme)
- post.AddContentVersion(row.Ver, row.Editor)
- post.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
-
- if row.ReplyPost != nil {
- reply := templates.PostToTemplate(row.ReplyPost, row.ReplyAuthor, c.Theme)
- reply.AddUrls(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
+ if p.ReplyPost != nil {
+ reply := templates.PostToTemplate(p.ReplyPost, p.ReplyAuthor, c.Theme)
+ addForumUrlsToPost(&reply, c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, post.ID)
post.ReplyPost = &reply
}
@@ -531,7 +502,7 @@ func ForumThread(c *RequestContext) ResponseData {
var res ResponseData
res.MustWriteTemplate("forum_thread.html", forumThreadData{
BaseData: baseData,
- Thread: templates.ThreadToTemplate(thread),
+ Thread: templates.ThreadToTemplate(&thread),
Posts: posts,
SubforumUrl: hmnurl.BuildForum(c.CurrentProject.Slug, currentSubforumSlugs, 1),
ReplyUrl: hmnurl.BuildForumPostReply(c.CurrentProject.Slug, currentSubforumSlugs, thread.ID, *thread.FirstID),
@@ -704,7 +675,7 @@ func ForumPostReply(c *RequestContext) ResponseData {
return FourOhFour(c)
}
- postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
+ postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c)
baseData.Title = fmt.Sprintf("Replying to \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
@@ -764,7 +735,7 @@ func ForumPostEdit(c *RequestContext) ResponseData {
return FourOhFour(c)
}
- postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
+ postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c)
baseData.Title = fmt.Sprintf("Editing \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
@@ -829,7 +800,7 @@ func ForumPostDelete(c *RequestContext) ResponseData {
return FourOhFour(c)
}
- postData := cd.FetchPostAndStuff(c.Context(), c.Conn)
+ postData := FetchPostAndStuff(c.Context(), c.Conn, cd.ThreadID, cd.PostID)
baseData := getBaseData(c)
baseData.Title = fmt.Sprintf("Deleting post in \"%s\" | %s", postData.Thread.Title, cd.SubforumTree[cd.SubforumID].Name)
@@ -1131,6 +1102,7 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
WHERE
id = $1
AND subforum_id = $2
+ AND NOT deleted
`,
res.ThreadID,
res.SubforumID,
@@ -1159,6 +1131,7 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
WHERE
id = $1
AND thread_id = $2
+ AND NOT deleted
`,
res.PostID,
res.ThreadID,
@@ -1175,114 +1148,6 @@ func getCommonForumData(c *RequestContext) (commonForumData, bool) {
return res, true
}
-/*
-Fetches the current thread according to the parsed path params. Since the ID was already validated
-in the constructor, this should always succeed.
-
-It will not, of course, succeed if there was no thread id in the path params, so don't do that.
-*/
-func (cd *commonForumData) FetchThread(ctx context.Context, connOrTx db.ConnOrTx) *models.Thread {
- cd.c.Perf.StartBlock("SQL", "Fetch current thread")
- type threadQueryResult struct {
- Thread models.Thread `db:"thread"`
- }
- irow, err := db.QueryOne(ctx, connOrTx, threadQueryResult{},
- `
- SELECT $columns
- FROM
- handmade_thread AS thread
- JOIN handmade_subforum AS sf ON sf.id = thread.subforum_id
- WHERE
- thread.id = $1
- AND NOT thread.deleted
- AND sf.id = $2
- `,
- cd.ThreadID,
- cd.SubforumID, // NOTE(asaf): This verifies that the requested thread is under the requested subforum.
- )
- cd.c.Perf.EndBlock()
- if err != nil {
- // We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
- panic(oops.New(err, "failed to fetch thread"))
- }
-
- thread := irow.(*threadQueryResult).Thread
- return &thread
-}
-
-type postAndRelatedModels struct {
- Thread models.Thread `db:"thread"`
- Post models.Post `db:"post"`
- CurrentVersion models.PostVersion `db:"ver"`
- Author *models.User `db:"author"`
- Editor *models.User `db:"editor"`
-}
-
-/*
-Fetches the post, the thread, and author / editor information for the post defined by the path
-params. Because all the IDs were validated in the constructor, this should always succeed.
-
-It will not succeed if there were missing path params though.
-*/
-func (cd *commonForumData) FetchPostAndStuff(ctx context.Context, connOrTx db.ConnOrTx) *postAndRelatedModels {
- cd.c.Perf.StartBlock("SQL", "Fetch post to reply to")
- postQueryResult, err := db.QueryOne(ctx, connOrTx, postAndRelatedModels{},
- `
- SELECT $columns
- FROM
- handmade_thread AS thread
- JOIN handmade_post AS post ON post.thread_id = thread.id
- JOIN handmade_postversion AS ver ON post.current_id = ver.id
- LEFT JOIN auth_user AS author ON post.author_id = author.id
- LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
- WHERE
- post.thread_id = $1
- AND post.id = $2
- AND NOT post.deleted
- `,
- cd.ThreadID,
- cd.PostID,
- )
- if err != nil {
- // We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
- panic(oops.New(err, "failed to fetch post and related data"))
- }
-
- result := postQueryResult.(*postAndRelatedModels)
- return result
-}
-
-func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.ConnOrTx, user models.User) bool {
- if user.IsStaff {
- return true
- }
-
- type postResult struct {
- AuthorID *int `db:"post.author_id"`
- }
- iresult, err := db.QueryOne(ctx, connOrTx, postResult{},
- `
- SELECT $columns
- FROM
- handmade_post AS post
- WHERE
- post.id = $1
- AND NOT post.deleted
- `,
- cd.PostID,
- )
- if err != nil {
- if errors.Is(err, db.ErrNoMatchingRows) {
- return false
- } else {
- panic(oops.New(err, "failed to get author of post when checking permissions"))
- }
- }
- result := iresult.(*postResult)
-
- return result.AuthorID != nil && *result.AuthorID == user.ID
-}
-
func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *models.Project, sfPath string) (int, bool) {
if project.ForumID == nil {
return -1, false
@@ -1316,3 +1181,10 @@ func validateSubforums(lineageBuilder *models.SubforumLineageBuilder, project *m
}
return subforumId, valid
}
+
+func addForumUrlsToPost(p *templates.Post, projectSlug string, subforums []string, threadId int, postId int) {
+ p.Url = hmnurl.BuildForumPost(projectSlug, subforums, threadId, postId)
+ p.DeleteUrl = hmnurl.BuildForumPostDelete(projectSlug, subforums, threadId, postId)
+ p.EditUrl = hmnurl.BuildForumPostEdit(projectSlug, subforums, threadId, postId)
+ p.ReplyUrl = hmnurl.BuildForumPostReply(projectSlug, subforums, threadId, postId)
+}
diff --git a/src/website/landing.go b/src/website/landing.go
index 0ba76df..b0877eb 100644
--- a/src/website/landing.go
+++ b/src/website/landing.go
@@ -156,7 +156,7 @@ func Index(c *RequestContext) ResponseData {
featurable := (!proj.IsHMN() &&
projectPost.Post.ThreadType == models.ThreadTypeProjectArticle &&
- projectPost.Post.ParentID == nil &&
+ *projectPost.Thread.FirstID == projectPost.Post.ID &&
landingPageProject.FeaturedPost == nil)
if featurable {
diff --git a/src/website/post_helper.go b/src/website/post_helper.go
index 93afa22..6adf487 100644
--- a/src/website/post_helper.go
+++ b/src/website/post_helper.go
@@ -19,6 +19,7 @@ func UrlForGenericPost(thread *models.Thread, post *models.Post, lineageBuilder
}
var PostTypeMap = map[models.ThreadType][]templates.PostType{
+ // { First post , Subsequent post }
models.ThreadTypeProjectArticle: {templates.PostTypeBlogPost, templates.PostTypeBlogComment},
models.ThreadTypeForumPost: {templates.PostTypeForumThread, templates.PostTypeForumReply},
}
@@ -76,11 +77,11 @@ func MakePostListItem(
postType := templates.PostTypeUnknown
postTypeOptions, found := PostTypeMap[post.ThreadType]
if found {
- var hasParent int
- if post.ParentID != nil {
- hasParent = 1
+ isNotFirst := 0
+ if *thread.FirstID != post.ID {
+ isNotFirst = 1
}
- postType = postTypeOptions[hasParent]
+ postType = postTypeOptions[isNotFirst]
}
result.PostType = postType
result.PostTypePrefix = PostTypePrefix[result.PostType]
diff --git a/src/website/routes.go b/src/website/routes.go
index 3780478..25cc438 100644
--- a/src/website/routes.go
+++ b/src/website/routes.go
@@ -166,6 +166,9 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
+ mainRoutes.GET(hmnurl.RegexBlogThread, BlogThread)
+ mainRoutes.GET(hmnurl.RegexBlogPost, BlogPostRedirectToThread)
+
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
mainRoutes.POST(hmnurl.RegexPodcastEdit, PodcastEditSubmit)
diff --git a/src/website/threads_and_posts_helper.go b/src/website/threads_and_posts_helper.go
new file mode 100644
index 0000000..75bfc44
--- /dev/null
+++ b/src/website/threads_and_posts_helper.go
@@ -0,0 +1,219 @@
+package website
+
+import (
+ "context"
+ "errors"
+ "math"
+
+ "git.handmade.network/hmn/hmn/src/db"
+ "git.handmade.network/hmn/hmn/src/models"
+ "git.handmade.network/hmn/hmn/src/oops"
+)
+
+type postAndRelatedModels struct {
+ Thread models.Thread
+ Post models.Post
+ CurrentVersion models.PostVersion
+
+ Author *models.User
+ Editor *models.User
+
+ ReplyPost *models.Post
+ ReplyAuthor *models.User
+}
+
+/*
+Fetches the thread defined by your (already parsed) path params.
+
+YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
+not check, for example, that the thread belongs to the correct subforum.
+*/
+func FetchThread(ctx context.Context, connOrTx db.ConnOrTx, threadId int) models.Thread {
+ type threadQueryResult struct {
+ Thread models.Thread `db:"thread"`
+ }
+ irow, err := db.QueryOne(ctx, connOrTx, threadQueryResult{},
+ `
+ SELECT $columns
+ FROM
+ handmade_thread AS thread
+ WHERE
+ id = $1
+ AND NOT deleted
+ `,
+ threadId,
+ )
+ if err != nil {
+ // We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
+ panic(oops.New(err, "failed to fetch thread"))
+ }
+
+ thread := irow.(*threadQueryResult).Thread
+ return thread
+}
+
+/*
+Fetches the post, the thread, and author / editor information for the post defined in
+your path params.
+
+YOU MUST VERIFY THAT THE THREAD ID AND POST ID ARE VALID BEFORE CALLING THIS FUNCTION.
+It will not check that the post belongs to the correct subforum, for example, or the
+correct project blog. This logic varies per route and per use of threads, so it doesn't
+happen here.
+*/
+func FetchPostAndStuff(
+ ctx context.Context,
+ connOrTx db.ConnOrTx,
+ threadId, postId int,
+) postAndRelatedModels {
+ type resultRow struct {
+ Thread models.Thread `db:"thread"`
+ Post models.Post `db:"post"`
+ CurrentVersion models.PostVersion `db:"ver"`
+ Author *models.User `db:"author"`
+ Editor *models.User `db:"editor"`
+ ReplyPost *models.Post `db:"reply"`
+ ReplyAuthor *models.User `db:"reply_author"`
+ }
+ postQueryResult, err := db.QueryOne(ctx, connOrTx, resultRow{},
+ `
+ SELECT $columns
+ FROM
+ handmade_thread AS thread
+ JOIN handmade_post AS post ON post.thread_id = thread.id
+ JOIN handmade_postversion AS ver ON post.current_id = ver.id
+ LEFT JOIN auth_user AS author ON post.author_id = author.id
+ LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
+ LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
+ LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
+ WHERE
+ post.thread_id = $1
+ AND post.id = $2
+ AND NOT post.deleted
+ `,
+ threadId,
+ postId,
+ )
+ if err != nil {
+ // We shouldn't encounter db.ErrNoMatchingRows, because validation should have verified that everything exists.
+ panic(oops.New(err, "failed to fetch post and related data"))
+ }
+
+ result := postQueryResult.(*resultRow)
+ return postAndRelatedModels{
+ Thread: result.Thread,
+ Post: result.Post,
+ CurrentVersion: result.CurrentVersion,
+ Author: result.Author,
+ Editor: result.Editor,
+ ReplyPost: result.ReplyPost,
+ ReplyAuthor: result.ReplyAuthor,
+ }
+}
+
+/*
+Fetches all the posts (and related models) for a given thread.
+
+YOU MUST VERIFY THAT THE THREAD ID IS VALID BEFORE CALLING THIS FUNCTION. It will
+not check, for example, that the thread belongs to the correct subforum.
+*/
+func FetchThreadPostsAndStuff(
+ ctx context.Context,
+ connOrTx db.ConnOrTx,
+ threadId int,
+ page, postsPerPage int,
+) (models.Thread, []postAndRelatedModels) {
+ limit := postsPerPage
+ offset := (page - 1) * postsPerPage
+ if postsPerPage == 0 {
+ limit = math.MaxInt32
+ offset = 0
+ }
+
+ thread := FetchThread(ctx, connOrTx, threadId)
+
+ type postResult struct {
+ Post models.Post `db:"post"`
+ CurrentVersion models.PostVersion `db:"ver"`
+ Author *models.User `db:"author"`
+ Editor *models.User `db:"editor"`
+ ReplyPost *models.Post `db:"reply"`
+ ReplyAuthor *models.User `db:"reply_author"`
+ }
+ itPosts, err := db.Query(ctx, connOrTx, postResult{},
+ `
+ SELECT $columns
+ FROM
+ handmade_post AS post
+ JOIN handmade_postversion AS ver ON post.current_id = ver.id
+ LEFT JOIN auth_user AS author ON post.author_id = author.id
+ LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
+ LEFT JOIN handmade_post AS reply ON post.reply_id = reply.id
+ LEFT JOIN auth_user AS reply_author ON reply.author_id = reply_author.id
+ WHERE
+ post.thread_id = $1
+ AND NOT post.deleted
+ ORDER BY post.postdate
+ LIMIT $2 OFFSET $3
+ `,
+ thread.ID,
+ limit,
+ offset,
+ )
+ if err != nil {
+ panic(oops.New(err, "failed to fetch posts for thread"))
+ }
+ defer itPosts.Close()
+
+ var posts []postAndRelatedModels
+ for {
+ irow, hasNext := itPosts.Next()
+ if !hasNext {
+ break
+ }
+
+ row := irow.(*postResult)
+ posts = append(posts, postAndRelatedModels{
+ Thread: thread,
+ Post: row.Post,
+ CurrentVersion: row.CurrentVersion,
+ Author: row.Author,
+ Editor: row.Editor,
+ ReplyPost: row.ReplyPost,
+ ReplyAuthor: row.ReplyAuthor,
+ })
+ }
+
+ return thread, posts
+}
+
+func (cd *commonForumData) UserCanEditPost(ctx context.Context, connOrTx db.ConnOrTx, user models.User) bool {
+ if user.IsStaff {
+ return true
+ }
+
+ type postResult struct {
+ AuthorID *int `db:"post.author_id"`
+ }
+ iresult, err := db.QueryOne(ctx, connOrTx, postResult{},
+ `
+ SELECT $columns
+ FROM
+ handmade_post AS post
+ WHERE
+ post.id = $1
+ AND NOT post.deleted
+ `,
+ cd.PostID,
+ )
+ if err != nil {
+ if errors.Is(err, db.ErrNoMatchingRows) {
+ return false
+ } else {
+ panic(oops.New(err, "failed to get author of post when checking permissions"))
+ }
+ }
+ result := iresult.(*postResult)
+
+ return result.AuthorID != nil && *result.AuthorID == user.ID
+}
diff --git a/src/website/timeline_helper.go b/src/website/timeline_helper.go
index 6d6eab6..b7d7c40 100644
--- a/src/website/timeline_helper.go
+++ b/src/website/timeline_helper.go
@@ -11,7 +11,7 @@ import (
)
var TimelineTypeMap = map[models.ThreadType][]templates.TimelineType{
- // { No parent , Has parent }
+ // { First post , Subsequent post }
models.ThreadTypeProjectArticle: {templates.TimelineTypeBlogPost, templates.TimelineTypeBlogComment},
models.ThreadTypeForumPost: {templates.TimelineTypeForumThread, templates.TimelineTypeForumReply},
}
@@ -50,11 +50,11 @@ func PostToTimelineItem(lineageBuilder *models.SubforumLineageBuilder, post *mod
itemType := templates.TimelineTypeUnknown
typeByCatKind, found := TimelineTypeMap[post.ThreadType]
if found {
- hasParent := 0
- if post.ParentID != nil {
- hasParent = 1
+ isNotFirst := 0
+ if *thread.FirstID != post.ID {
+ isNotFirst = 1
}
- itemType = typeByCatKind[hasParent]
+ itemType = typeByCatKind[isNotFirst]
}
return templates.TimelineItem{