Go to Comment endpoint + notification fixes

* Add endpoint /go/comment?id= that finds the right page that a comment
  can be seen on for the current user and redirects there.
* Resolves issues with link discrepancies in comment notifications, if
  the recipient sees different page numbers depending on blocklist
  status.
* Supports copyable permalinks to any comment on the site reliably.
face-detect
Noah Petherbridge 2024-01-05 22:14:42 -08:00
parent 7555dee944
commit 70402b42c9
7 changed files with 146 additions and 4 deletions

View File

@ -0,0 +1,75 @@
package comment
import (
"fmt"
"net/http"
"strconv"
"code.nonshy.com/nonshy/website/pkg/config"
"code.nonshy.com/nonshy/website/pkg/models"
"code.nonshy.com/nonshy/website/pkg/session"
"code.nonshy.com/nonshy/website/pkg/templates"
)
// GoToComment finds the correct link to view a comment.
func GoToComment() http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Query params.
var (
commentID uint64
)
// Parse the ID param.
if idStr := r.FormValue("id"); idStr == "" {
session.FlashError(w, r, "Comment ID required.")
templates.Redirect(w, "/")
return
} else {
if idInt, err := strconv.Atoi(idStr); err != nil {
session.FlashError(w, r, "Comment ID invalid.")
templates.Redirect(w, "/")
return
} else {
commentID = uint64(idInt)
}
}
// Get the current user.
currentUser, err := session.CurrentUser(r)
if err != nil {
session.FlashError(w, r, "Couldn't get current user: %s", err)
templates.Redirect(w, "/")
return
}
// Locate this comment.
comment, err := models.GetComment(commentID)
if err != nil {
session.FlashError(w, r, "Couldn't find that comment: %s", err)
templates.Redirect(w, "/")
return
}
// Where is this comment at?
switch comment.TableName {
case "threads":
// A forum thread, find what page it is on.
page, err := models.FindPageByComment(currentUser, comment, config.PageSizeThreadList)
if err != nil {
session.FlashError(w, r, "Couldn't find that comment, but bringing you to the forum thread instead.")
page = 1
}
templates.Redirect(w, fmt.Sprintf("/forum/thread/%d?page=%d#p%d", comment.TableID, page, comment.ID))
return
case "photos":
// A photo comment thread, easy: only one page.
templates.Redirect(w, fmt.Sprintf("/photo/view?id=%d#p%d", comment.TableID, comment.ID))
return
default:
session.FlashError(w, r, "Unknown type of comment.")
}
templates.Redirect(w, "/")
})
}

View File

@ -177,7 +177,7 @@ func PostComment() http.HandlerFunc {
TableName: comment.TableName,
TableID: comment.TableID,
Message: message,
Link: fmt.Sprintf("%s#p%d", fromURL, comment.ID),
Link: fmt.Sprintf("/go/comment?id=%d", comment.ID),
}
if err := models.CreateNotification(notif); err != nil {
log.Error("Couldn't create Comment notification: %s", err)
@ -201,7 +201,7 @@ func PostComment() http.HandlerFunc {
TableName: comment.TableName,
TableID: comment.TableID,
Message: message,
Link: fromURL,
Link: fmt.Sprintf("/go/comment?id=%d", comment.ID),
}
if err := models.CreateNotification(notif); err != nil {
log.Error("Couldn't create Comment notification for subscriber %d: %s", userID, err)

View File

@ -1,6 +1,8 @@
package models
import (
"errors"
"math"
"strings"
"time"
@ -108,6 +110,50 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
return cs, result.Error
}
// FindPageByComment finds out what page a comment ID exists on for the current user, taking into
// account their block lists and comment visibility.
//
// Note: the comments are assumed ordered by created_at asc.
func FindPageByComment(user *User, comment *Comment, pageSize int) (int, error) {
var (
allCommentIDs []uint64
blockedUserIDs = BlockedUserIDs(user)
wheres = []string{}
placeholders = []interface{}{}
)
// Get the complete set of comment IDs that this comment is on a thread of.
wheres = append(wheres, "table_name = ? AND table_id = ?")
placeholders = append(placeholders, comment.TableName, comment.TableID)
if len(blockedUserIDs) > 0 {
wheres = append(wheres, "user_id NOT IN ?")
placeholders = append(placeholders, blockedUserIDs)
}
result := DB.Table(
"comments",
).Select(
"id",
).Where(
strings.Join(wheres, " AND "),
placeholders...,
).Order("created_at asc").Scan(&allCommentIDs)
if result.Error != nil {
return 0, result.Error
}
// Scan the comment thread to find it.
for i, cid := range allCommentIDs {
if cid == comment.ID {
var page = int(math.Ceil(float64(i) / float64(pageSize)))
return page, nil
}
}
return -1, errors.New("comment not visible to current user")
}
// ListComments returns a complete set of comments without paging.
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
var (

View File

@ -112,6 +112,9 @@ func New() http.Handler {
mux.Handle("/v1/barertc/report", barertc.Report())
mux.Handle("/v1/barertc/profile", barertc.Profile())
// Redirect endpoints.
mux.Handle("/go/comment", middleware.LoginRequired(comment.GoToComment()))
// Static files.
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(config.StaticPath))))

View File

@ -105,7 +105,7 @@
<div class="columns is-multiline is-gapless mb-0">
<div class="column is-narrow">
<a href="/forum/thread/{{.ThreadID}}?page=-1" class="has-text-grey">
<a href="/go/comment?id={{.Comment.ID}}" class="has-text-grey">
<h2 class="is-size-5 py-0 is-italic has-text-grey">
<span class="icon"><i class="fa fa-reply"></i></span>
Comment added by
@ -131,7 +131,7 @@
</div>
<a href="/forum/thread/{{.ThreadID}}?page=-1" class="has-text-dark">
<a href="/go/comment?id={{.Comment.ID}}" class="has-text-dark">
{{TrimEllipses .Comment.Message 256}}
</a>

View File

@ -298,6 +298,15 @@
</a>
</div>
{{end}}
<!-- Copy link to clipboard -->
<div class="column is-narrow">
<a href="/go/comment?id={{.ID}}" onclick="navigator.clipboard.writeText(this.href); window.location='#p{{.ID}}'; return false"
class="has-text-dark"
title="Copy link to clipboard">
<span class="icon"><i class="fa fa-paragraph"></i></span>
</a>
</div>
</div>
{{if $Root.CurrentUser.IsAdmin}}

View File

@ -289,6 +289,15 @@
</a>
</div>
{{end}}
<!-- Copy link to clipboard -->
<div class="column is-narrow">
<a href="/go/comment?id={{.ID}}" onclick="navigator.clipboard.writeText(this.href); window.location='#p{{.ID}}'; return false"
class="has-text-dark"
title="Copy link to clipboard">
<span class="icon"><i class="fa fa-paragraph"></i></span>
</a>
</div>
</div>
</div>
</div>