Add CLRI/TLRI handling
This commit is contained in:
parent
94457aeb93
commit
9b9d467ce4
|
@ -4690,7 +4690,11 @@ code, .code {
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0; }
|
padding-bottom: 0; }
|
||||||
|
|
||||||
.pv1, .optionbar .options > * {
|
.pv1,
|
||||||
|
.optionbar .options button,
|
||||||
|
.optionbar .options .button,
|
||||||
|
.optionbar .options input[type=button],
|
||||||
|
.optionbar .options input[type=submit] {
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
padding-bottom: 0.25rem; }
|
padding-bottom: 0.25rem; }
|
||||||
|
|
||||||
|
@ -4730,7 +4734,11 @@ input[type=submit] {
|
||||||
padding-left: 0.25rem;
|
padding-left: 0.25rem;
|
||||||
padding-right: 0.25rem; }
|
padding-right: 0.25rem; }
|
||||||
|
|
||||||
.ph2, .optionbar .options > *, .pagination .button {
|
.ph2,
|
||||||
|
.optionbar .options button,
|
||||||
|
.optionbar .options .button,
|
||||||
|
.optionbar .options input[type=button],
|
||||||
|
.optionbar .options input[type=submit], .pagination .button {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem; }
|
padding-right: 0.5rem; }
|
||||||
|
|
||||||
|
@ -5029,7 +5037,11 @@ input[type=submit] {
|
||||||
.pv1-ns {
|
.pv1-ns {
|
||||||
padding-top: 0.25rem;
|
padding-top: 0.25rem;
|
||||||
padding-bottom: 0.25rem; }
|
padding-bottom: 0.25rem; }
|
||||||
.pv2-ns, .optionbar .options > * {
|
.pv2-ns,
|
||||||
|
.optionbar .options button,
|
||||||
|
.optionbar .options .button,
|
||||||
|
.optionbar .options input[type=button],
|
||||||
|
.optionbar .options input[type=submit] {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem; }
|
padding-bottom: 0.5rem; }
|
||||||
.pv3-ns, header .menu-bar .items a {
|
.pv3-ns, header .menu-bar .items a {
|
||||||
|
@ -7740,14 +7752,17 @@ header {
|
||||||
@media screen and (min-width: 30em) {
|
@media screen and (min-width: 30em) {
|
||||||
.optionbar .options {
|
.optionbar .options {
|
||||||
flex-direction: row; } }
|
flex-direction: row; } }
|
||||||
|
|
||||||
.optionbar button,
|
.optionbar .options button,
|
||||||
.optionbar .button,
|
.optionbar .options .button,
|
||||||
.optionbar input[type=button],
|
.optionbar .options input[type=button],
|
||||||
.optionbar input[type=submit] {
|
.optionbar .options input[type=submit] {
|
||||||
border: none;
|
display: inline-flex;
|
||||||
background: none;
|
border: none;
|
||||||
font-weight: normal; }
|
background: none;
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit; }
|
||||||
.optionbar .group {
|
.optionbar .group {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -8447,9 +8462,12 @@ input[type=submit] {
|
||||||
.button.lite,
|
.button.lite,
|
||||||
input[type=button].lite,
|
input[type=button].lite,
|
||||||
input[type=submit].lite {
|
input[type=submit].lite {
|
||||||
|
display: inline-flex;
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
font-weight: normal; }
|
font-weight: normal;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit; }
|
||||||
|
|
||||||
button.inline-button,
|
button.inline-button,
|
||||||
.button.inline-button,
|
.button.inline-button,
|
||||||
|
|
|
@ -395,8 +395,8 @@ func TestPublic(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
|
AssertRegexMatch(t, BuildUserFile("mylogo.png"), RegexPublic, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMarkRead(t *testing.T) {
|
func TestForumCategoryMarkRead(t *testing.T) {
|
||||||
AssertRegexMatch(t, BuildMarkRead(5), RegexMarkRead, map[string]string{"catid": "5"})
|
AssertRegexMatch(t, BuildForumCategoryMarkRead(5), RegexForumCategoryMarkRead, map[string]string{"catid": "5"})
|
||||||
}
|
}
|
||||||
|
|
||||||
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
func AssertSubdomain(t *testing.T, fullUrl string, expectedSubdomain string) {
|
||||||
|
|
|
@ -803,17 +803,17 @@ func BuildUserFile(filepath string) string {
|
||||||
* Other
|
* Other
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var RegexMarkRead = regexp.MustCompile(`^/_markread/(?P<catid>\d+)$`)
|
var RegexForumCategoryMarkRead = regexp.MustCompile(`^/markread/(?P<catid>\d+)$`)
|
||||||
|
|
||||||
// NOTE(asaf): categoryId == 0 means ALL CATEGORIES
|
// NOTE(asaf): categoryId == 0 means ALL CATEGORIES
|
||||||
func BuildMarkRead(categoryId int) string {
|
func BuildForumCategoryMarkRead(categoryId int) string {
|
||||||
defer CatchPanic()
|
defer CatchPanic()
|
||||||
if categoryId < 0 {
|
if categoryId < 0 {
|
||||||
panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId))
|
panic(oops.New(nil, "Invalid category ID (%d), must be >= 0", categoryId))
|
||||||
}
|
}
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString("/_markread/")
|
builder.WriteString("/markread/")
|
||||||
builder.WriteString(strconv.Itoa(categoryId))
|
builder.WriteString(strconv.Itoa(categoryId))
|
||||||
|
|
||||||
return Url(builder.String(), nil)
|
return Url(builder.String(), nil)
|
||||||
|
|
|
@ -25,7 +25,7 @@ var PlaintextMarkdown = goldmark.New(
|
||||||
goldmark.WithRenderer(plaintextRenderer{}),
|
goldmark.WithRenderer(plaintextRenderer{}),
|
||||||
)
|
)
|
||||||
|
|
||||||
func ParsePostInput(source string, md goldmark.Markdown) string {
|
func ParseMarkdown(source string, md goldmark.Markdown) string {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := md.Convert([]byte(source), &buf); err != nil {
|
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -10,14 +10,14 @@ import (
|
||||||
func TestMarkdown(t *testing.T) {
|
func TestMarkdown(t *testing.T) {
|
||||||
t.Run("fenced code blocks", func(t *testing.T) {
|
t.Run("fenced code blocks", func(t *testing.T) {
|
||||||
t.Run("multiple lines", func(t *testing.T) {
|
t.Run("multiple lines", func(t *testing.T) {
|
||||||
html := ParsePostInput("```\nmultiple lines\n\tof code\n```", RealMarkdown)
|
html := ParseMarkdown("```\nmultiple lines\n\tof code\n```", RealMarkdown)
|
||||||
t.Log(html)
|
t.Log(html)
|
||||||
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
||||||
assert.Contains(t, html, `class="hmn-code"`)
|
assert.Contains(t, html, `class="hmn-code"`)
|
||||||
assert.Contains(t, html, "multiple lines\n\tof code")
|
assert.Contains(t, html, "multiple lines\n\tof code")
|
||||||
})
|
})
|
||||||
t.Run("multiple lines with language", func(t *testing.T) {
|
t.Run("multiple lines with language", func(t *testing.T) {
|
||||||
html := ParsePostInput("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", RealMarkdown)
|
html := ParseMarkdown("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", RealMarkdown)
|
||||||
t.Log(html)
|
t.Log(html)
|
||||||
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
||||||
assert.Contains(t, html, `class="hmn-code"`)
|
assert.Contains(t, html, `class="hmn-code"`)
|
||||||
|
@ -30,7 +30,7 @@ func TestMarkdown(t *testing.T) {
|
||||||
func TestBBCode(t *testing.T) {
|
func TestBBCode(t *testing.T) {
|
||||||
t.Run("[code]", func(t *testing.T) {
|
t.Run("[code]", func(t *testing.T) {
|
||||||
t.Run("one line", func(t *testing.T) {
|
t.Run("one line", func(t *testing.T) {
|
||||||
html := ParsePostInput("[code]Just some code, you know?[/code]", RealMarkdown)
|
html := ParseMarkdown("[code]Just some code, you know?[/code]", RealMarkdown)
|
||||||
t.Log(html)
|
t.Log(html)
|
||||||
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
||||||
assert.Contains(t, html, `class="hmn-code"`)
|
assert.Contains(t, html, `class="hmn-code"`)
|
||||||
|
@ -41,7 +41,7 @@ func TestBBCode(t *testing.T) {
|
||||||
Multiline code
|
Multiline code
|
||||||
with an indent
|
with an indent
|
||||||
[/code]`
|
[/code]`
|
||||||
html := ParsePostInput(bbcode, RealMarkdown)
|
html := ParseMarkdown(bbcode, RealMarkdown)
|
||||||
t.Log(html)
|
t.Log(html)
|
||||||
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
||||||
assert.Contains(t, html, `class="hmn-code"`)
|
assert.Contains(t, html, `class="hmn-code"`)
|
||||||
|
@ -54,7 +54,7 @@ func main() {
|
||||||
fmt.Println("Hello, world!")
|
fmt.Println("Hello, world!")
|
||||||
}
|
}
|
||||||
[/code]`
|
[/code]`
|
||||||
html := ParsePostInput(bbcode, RealMarkdown)
|
html := ParseMarkdown(bbcode, RealMarkdown)
|
||||||
t.Log(html)
|
t.Log(html)
|
||||||
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
assert.Equal(t, 1, strings.Count(html, "<pre"))
|
||||||
assert.Contains(t, html, "Println")
|
assert.Contains(t, html, "Println")
|
||||||
|
@ -66,7 +66,7 @@ func main() {
|
||||||
func TestSharlock(t *testing.T) {
|
func TestSharlock(t *testing.T) {
|
||||||
t.Skipf("This doesn't pass right now because parts of Sharlock's original source read as indented code blocks, or depend on different line break behavior.")
|
t.Skipf("This doesn't pass right now because parts of Sharlock's original source read as indented code blocks, or depend on different line break behavior.")
|
||||||
t.Run("sanity check", func(t *testing.T) {
|
t.Run("sanity check", func(t *testing.T) {
|
||||||
result := ParsePostInput(sharlock, RealMarkdown)
|
result := ParseMarkdown(sharlock, RealMarkdown)
|
||||||
|
|
||||||
for _, line := range strings.Split(result, "\n") {
|
for _, line := range strings.Split(result, "\n") {
|
||||||
assert.NotContains(t, line, "[b]")
|
assert.NotContains(t, line, "[b]")
|
||||||
|
@ -85,6 +85,6 @@ func TestSharlock(t *testing.T) {
|
||||||
|
|
||||||
func BenchmarkSharlock(b *testing.B) {
|
func BenchmarkSharlock(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
ParsePostInput(sharlock, RealMarkdown)
|
ParseMarkdown(sharlock, RealMarkdown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
return parsing.ParsePostInput(args[0].String(), parsing.PreviewMarkdown)
|
return parsing.ParseMarkdown(args[0].String(), parsing.PreviewMarkdown)
|
||||||
}))
|
}))
|
||||||
|
|
||||||
var done chan bool
|
var done chan bool
|
||||||
|
|
|
@ -702,17 +702,16 @@ footer {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > * {
|
& {
|
||||||
@extend .ph2;
|
#{$buttons} {
|
||||||
@extend .pv1;
|
@include lite-button;
|
||||||
@extend .pv2-ns;
|
@extend .ph2;
|
||||||
|
@extend .pv1;
|
||||||
|
@extend .pv2-ns;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#{$buttons} {
|
|
||||||
@include lite-button;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
.group {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
@ -148,7 +148,7 @@ input, select, textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.lite {
|
&.lite {
|
||||||
@include lite-button;
|
@include lite-button;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UNUSED
|
// UNUSED
|
||||||
|
|
|
@ -45,9 +45,12 @@ will throw an error.
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin lite-button {
|
@mixin lite-button {
|
||||||
border: none;
|
display: inline-flex;
|
||||||
background: none;
|
border: none;
|
||||||
font-weight: normal;
|
background: none;
|
||||||
|
font-weight: normal;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
$buttons: "
|
$buttons: "
|
||||||
|
|
|
@ -10,9 +10,12 @@
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<div class="optionbar">
|
<div class="optionbar">
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big">4</span> RSS Feed</span></a>
|
<a class="button" href="{{ .AtomFeedUrl }}"><span class="icon big pr1">4</span> RSS Feed</span></a>
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a class="button" href="{{ .MarkAllReadUrl }}"><span class="big">✓</span> Mark all posts on site as read</a>
|
<form method="POST" action="{{ .MarkAllReadUrl }}">
|
||||||
|
{{ csrftoken .Session }}
|
||||||
|
<button type="submit"><span class="big pr1">✓</span> Mark all posts on site as read</button>
|
||||||
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div class="options">
|
<div class="options">
|
||||||
|
|
|
@ -35,9 +35,11 @@
|
||||||
{{ define "forum_category_options" }}
|
{{ define "forum_category_options" }}
|
||||||
<div class="options">
|
<div class="options">
|
||||||
{{ if .User }}
|
{{ if .User }}
|
||||||
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big">+</span> New Thread</a>
|
<a class="button new-thread" href="{{ .NewThreadUrl }}"><span class="big pr1">+</span> New Thread</a>
|
||||||
{{/* TODO(asaf): Mark read should probably be a POST, since it's destructive and we would probably want CSRF for it */}}
|
<form method="POST" action="{{ .MarkReadUrl }}">
|
||||||
<a class="button" href="{{ .MarkReadUrl }}"><span class="big">✓</span> Mark threads here as read</a>
|
{{ csrftoken .Session }}
|
||||||
|
<button type="submit"><span class="big pr1">✓</span> Mark threads here as read</button>
|
||||||
|
</form>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<a class="button" href="{{ .LoginPageUrl }}">Log in to post a new thread</a>
|
<a class="button" href="{{ .LoginPageUrl }}">Log in to post a new thread</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -3,8 +3,12 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="content-block">
|
<div class="content-block">
|
||||||
<div class="optionbar">
|
<div class="optionbar">
|
||||||
<a class="button" href="{{ .CategoryUrl }}">← Back to index</a>
|
<div class="options">
|
||||||
{{ template "pagination.html" .Pagination }}
|
<a class="button" href="{{ .CategoryUrl }}">← Back to index</a>
|
||||||
|
</div>
|
||||||
|
<div class="options">
|
||||||
|
{{ template "pagination.html" .Pagination }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ range .Posts }}
|
{{ range .Posts }}
|
||||||
<div class="post background-even pa3">
|
<div class="post background-even pa3">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{- /*gotype: git.handmade.network/hmn/hmn/src/templates.Pagination*/ -}}
|
{{- /*gotype: git.handmade.network/hmn/hmn/src/templates.Pagination*/ -}}
|
||||||
<div class="pagination">
|
<div class="pagination flex">
|
||||||
{{ if gt .Current 1 }}
|
{{ if gt .Current 1 }}
|
||||||
<a class="button" href="{{ .PreviousUrl }}">Prev</a>
|
<a class="button" href="{{ .PreviousUrl }}">Prev</a>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func Feed(c *RequestContext) ResponseData {
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
|
|
||||||
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
AtomFeedUrl: hmnurl.BuildAtomFeed(),
|
||||||
MarkAllReadUrl: hmnurl.BuildMarkRead(0),
|
MarkAllReadUrl: hmnurl.BuildForumCategoryMarkRead(0),
|
||||||
Posts: posts,
|
Posts: posts,
|
||||||
Pagination: pagination,
|
Pagination: pagination,
|
||||||
}, c.Perf)
|
}, c.Perf)
|
||||||
|
|
|
@ -279,7 +279,7 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
res.MustWriteTemplate("forum_category.html", forumCategoryData{
|
res.MustWriteTemplate("forum_category.html", forumCategoryData{
|
||||||
BaseData: baseData,
|
BaseData: baseData,
|
||||||
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
NewThreadUrl: hmnurl.BuildForumNewThread(c.CurrentProject.Slug, currentSubforumSlugs, false),
|
||||||
MarkReadUrl: hmnurl.BuildMarkRead(currentCatId),
|
MarkReadUrl: hmnurl.BuildForumCategoryMarkRead(currentCatId),
|
||||||
Threads: threads,
|
Threads: threads,
|
||||||
Pagination: templates.Pagination{
|
Pagination: templates.Pagination{
|
||||||
Current: page,
|
Current: page,
|
||||||
|
@ -295,6 +295,103 @@ func ForumCategory(c *RequestContext) ResponseData {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ForumCategoryMarkRead(c *RequestContext) ResponseData {
|
||||||
|
c.Perf.StartBlock("SQL", "Fetch category tree")
|
||||||
|
categoryTree := models.GetFullCategoryTree(c.Context(), c.Conn)
|
||||||
|
lineageBuilder := models.MakeCategoryLineageBuilder(categoryTree)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
|
catId, err := strconv.Atoi(c.PathParams["catid"])
|
||||||
|
if err != nil {
|
||||||
|
return FourOhFour(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := c.Conn.Begin(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(c.Context())
|
||||||
|
|
||||||
|
// TODO(ben): Rework this logic when we rework blogs, threads, etc.
|
||||||
|
catIds := []int{catId}
|
||||||
|
if catId == 0 {
|
||||||
|
// Select all categories
|
||||||
|
type catIdResult struct {
|
||||||
|
CatID int `db:"id"`
|
||||||
|
}
|
||||||
|
cats, err := db.Query(c.Context(), tx, catIdResult{},
|
||||||
|
`
|
||||||
|
SELECT $columns
|
||||||
|
FROM handmade_category
|
||||||
|
WHERE kind = ANY ($1)
|
||||||
|
`,
|
||||||
|
[]models.CategoryKind{models.CatKindBlog, models.CatKindForum, models.CatKindWiki, models.CatKindLibraryResource},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch category IDs for CLRI"))
|
||||||
|
}
|
||||||
|
|
||||||
|
catIdResults := cats.ToSlice()
|
||||||
|
catIds = make([]int, len(catIdResults))
|
||||||
|
for i, res := range catIdResults {
|
||||||
|
catIds[i] = res.(*catIdResult).CatID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Update CLRIs")
|
||||||
|
_, err = tx.Exec(c.Context(),
|
||||||
|
`
|
||||||
|
INSERT INTO handmade_categorylastreadinfo (category_id, user_id, lastread)
|
||||||
|
SELECT id, $2, $3
|
||||||
|
FROM handmade_category
|
||||||
|
WHERE id = ANY ($1)
|
||||||
|
ON CONFLICT (category_id, user_id) DO UPDATE
|
||||||
|
SET lastread = EXCLUDED.lastread
|
||||||
|
`,
|
||||||
|
catIds,
|
||||||
|
c.CurrentUser.ID,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum clris"))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Perf.StartBlock("SQL", "Delete TLRIs")
|
||||||
|
_, err = tx.Exec(c.Context(),
|
||||||
|
`
|
||||||
|
DELETE FROM handmade_threadlastreadinfo
|
||||||
|
WHERE
|
||||||
|
user_id = $2
|
||||||
|
AND thread_id IN (
|
||||||
|
SELECT id
|
||||||
|
FROM handmade_thread
|
||||||
|
WHERE
|
||||||
|
category_id = ANY ($1)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
catIds,
|
||||||
|
c.CurrentUser.ID,
|
||||||
|
)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to delete unnecessary tlris"))
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit(c.Context())
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to commit CLRI/TLRI updates"))
|
||||||
|
}
|
||||||
|
|
||||||
|
var redirUrl string
|
||||||
|
if catId == 0 {
|
||||||
|
redirUrl = hmnurl.BuildFeed()
|
||||||
|
} else {
|
||||||
|
redirUrl = hmnurl.BuildForumCategory(c.CurrentProject.Slug, lineageBuilder.GetSubforumLineageSlugs(catId), 1)
|
||||||
|
}
|
||||||
|
return c.Redirect(redirUrl, http.StatusSeeOther)
|
||||||
|
}
|
||||||
|
|
||||||
type forumThreadData struct {
|
type forumThreadData struct {
|
||||||
templates.BaseData
|
templates.BaseData
|
||||||
|
|
||||||
|
@ -435,6 +532,24 @@ func ForumThread(c *RequestContext) ResponseData {
|
||||||
posts = append(posts, post)
|
posts = append(posts, post)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update thread last read info
|
||||||
|
c.Perf.StartBlock("SQL", "Update TLRI")
|
||||||
|
_, err = c.Conn.Exec(c.Context(),
|
||||||
|
`
|
||||||
|
INSERT INTO handmade_threadlastreadinfo (thread_id, user_id, lastread)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (thread_id, user_id) DO UPDATE
|
||||||
|
SET lastread = EXCLUDED.lastread
|
||||||
|
`,
|
||||||
|
threadId,
|
||||||
|
c.CurrentUser.ID,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
c.Perf.EndBlock()
|
||||||
|
if err != nil {
|
||||||
|
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update forum tlri"))
|
||||||
|
}
|
||||||
|
|
||||||
baseData := getBaseData(c)
|
baseData := getBaseData(c)
|
||||||
baseData.Title = thread.Title
|
baseData.Title = thread.Title
|
||||||
// TODO(asaf): Set breadcrumbs
|
// TODO(asaf): Set breadcrumbs
|
||||||
|
@ -1132,11 +1247,11 @@ func createNewForumPostAndVersion(ctx context.Context, tx pgx.Tx, catId, threadI
|
||||||
}
|
}
|
||||||
|
|
||||||
func createForumPostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedContent string, ipString string, editReason string, editorId *int) (versionId int) {
|
func createForumPostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedContent string, ipString string, editReason string, editorId *int) (versionId int) {
|
||||||
parsed := parsing.ParsePostInput(unparsedContent, parsing.RealMarkdown)
|
parsed := parsing.ParseMarkdown(unparsedContent, parsing.RealMarkdown)
|
||||||
ip := net.ParseIP(ipString)
|
ip := net.ParseIP(ipString)
|
||||||
|
|
||||||
const previewMaxLength = 100
|
const previewMaxLength = 100
|
||||||
parsedPlaintext := parsing.ParsePostInput(unparsedContent, parsing.PlaintextMarkdown)
|
parsedPlaintext := parsing.ParseMarkdown(unparsedContent, parsing.PlaintextMarkdown)
|
||||||
preview := parsedPlaintext
|
preview := parsedPlaintext
|
||||||
if len(preview) > previewMaxLength-1 {
|
if len(preview) > previewMaxLength-1 {
|
||||||
preview = preview[:previewMaxLength-1] + "…"
|
preview = preview[:previewMaxLength-1] + "…"
|
||||||
|
|
|
@ -484,7 +484,7 @@ func PodcastEpisodeSubmit(c *RequestContext) ResponseData {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Perf.StartBlock("MARKDOWN", "Parsing description")
|
c.Perf.StartBlock("MARKDOWN", "Parsing description")
|
||||||
descriptionRendered := parsing.ParsePostInput(description, parsing.RealMarkdown)
|
descriptionRendered := parsing.ParseMarkdown(description, parsing.RealMarkdown)
|
||||||
c.Perf.EndBlock()
|
c.Perf.EndBlock()
|
||||||
|
|
||||||
guidStr := ""
|
guidStr := ""
|
||||||
|
|
|
@ -157,13 +157,14 @@ func NewWebsiteRoutes(conn *pgxpool.Pool, perfCollector *perf.PerfCollector) htt
|
||||||
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
mainRoutes.POST(hmnurl.RegexForumNewThreadSubmit, authMiddleware(csrfMiddleware(ForumNewThreadSubmit)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
mainRoutes.GET(hmnurl.RegexForumThread, ForumThread)
|
||||||
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
|
mainRoutes.GET(hmnurl.RegexForumCategory, ForumCategory)
|
||||||
|
mainRoutes.POST(hmnurl.RegexForumCategoryMarkRead, authMiddleware(csrfMiddleware(ForumCategoryMarkRead)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
mainRoutes.GET(hmnurl.RegexForumPost, ForumPostRedirect)
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
mainRoutes.GET(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReply))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(ForumPostReplySubmit))
|
mainRoutes.POST(hmnurl.RegexForumPostReply, authMiddleware(csrfMiddleware(ForumPostReplySubmit)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
|
mainRoutes.GET(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEdit))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostEdit, authMiddleware(ForumPostEditSubmit))
|
mainRoutes.POST(hmnurl.RegexForumPostEdit, authMiddleware(csrfMiddleware(ForumPostEditSubmit)))
|
||||||
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
mainRoutes.GET(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDelete))
|
||||||
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(ForumPostDeleteSubmit))
|
mainRoutes.POST(hmnurl.RegexForumPostDelete, authMiddleware(csrfMiddleware(ForumPostDeleteSubmit)))
|
||||||
|
|
||||||
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
|
mainRoutes.GET(hmnurl.RegexPodcast, PodcastIndex)
|
||||||
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
|
mainRoutes.GET(hmnurl.RegexPodcastEdit, PodcastEdit)
|
||||||
|
|
Loading…
Reference in New Issue