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.
This commit is contained in:
parent
7555dee944
commit
70402b42c9
75
pkg/controller/comment/goto_comment.go
Normal file
75
pkg/controller/comment/goto_comment.go
Normal 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, "/")
|
||||||
|
})
|
||||||
|
}
|
|
@ -177,7 +177,7 @@ func PostComment() http.HandlerFunc {
|
||||||
TableName: comment.TableName,
|
TableName: comment.TableName,
|
||||||
TableID: comment.TableID,
|
TableID: comment.TableID,
|
||||||
Message: message,
|
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 {
|
if err := models.CreateNotification(notif); err != nil {
|
||||||
log.Error("Couldn't create Comment notification: %s", err)
|
log.Error("Couldn't create Comment notification: %s", err)
|
||||||
|
@ -201,7 +201,7 @@ func PostComment() http.HandlerFunc {
|
||||||
TableName: comment.TableName,
|
TableName: comment.TableName,
|
||||||
TableID: comment.TableID,
|
TableID: comment.TableID,
|
||||||
Message: message,
|
Message: message,
|
||||||
Link: fromURL,
|
Link: fmt.Sprintf("/go/comment?id=%d", comment.ID),
|
||||||
}
|
}
|
||||||
if err := models.CreateNotification(notif); err != nil {
|
if err := models.CreateNotification(notif); err != nil {
|
||||||
log.Error("Couldn't create Comment notification for subscriber %d: %s", userID, err)
|
log.Error("Couldn't create Comment notification for subscriber %d: %s", userID, err)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -108,6 +110,50 @@ func PaginateComments(user *User, tableName string, tableID uint64, pager *Pagin
|
||||||
return cs, result.Error
|
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.
|
// ListComments returns a complete set of comments without paging.
|
||||||
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
|
func ListComments(user *User, tableName string, tableID uint64) ([]*Comment, error) {
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -112,6 +112,9 @@ func New() http.Handler {
|
||||||
mux.Handle("/v1/barertc/report", barertc.Report())
|
mux.Handle("/v1/barertc/report", barertc.Report())
|
||||||
mux.Handle("/v1/barertc/profile", barertc.Profile())
|
mux.Handle("/v1/barertc/profile", barertc.Profile())
|
||||||
|
|
||||||
|
// Redirect endpoints.
|
||||||
|
mux.Handle("/go/comment", middleware.LoginRequired(comment.GoToComment()))
|
||||||
|
|
||||||
// Static files.
|
// Static files.
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(config.StaticPath))))
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(config.StaticPath))))
|
||||||
|
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
|
|
||||||
<div class="columns is-multiline is-gapless mb-0">
|
<div class="columns is-multiline is-gapless mb-0">
|
||||||
<div class="column is-narrow">
|
<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">
|
<h2 class="is-size-5 py-0 is-italic has-text-grey">
|
||||||
<span class="icon"><i class="fa fa-reply"></i></span>
|
<span class="icon"><i class="fa fa-reply"></i></span>
|
||||||
Comment added by
|
Comment added by
|
||||||
|
@ -131,7 +131,7 @@
|
||||||
|
|
||||||
</div>
|
</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}}
|
{{TrimEllipses .Comment.Message 256}}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,15 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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>
|
||||||
|
|
||||||
{{if $Root.CurrentUser.IsAdmin}}
|
{{if $Root.CurrentUser.IsAdmin}}
|
||||||
|
|
|
@ -289,6 +289,15 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user