HTMX lazy load for user statistics card
This commit is contained in:
parent
e7f7f4d0d3
commit
f4721d65da
|
@ -79,14 +79,9 @@ func Profile() http.HandlerFunc {
|
|||
|
||||
var isSelf = currentUser.ID == user.ID
|
||||
|
||||
// Banned or disabled? Only admin can view then.
|
||||
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Is either one blocking?
|
||||
if models.IsBlocking(currentUser.ID, user.ID) && !currentUser.IsAdmin {
|
||||
// Give a Not Found page if we can not see this user.
|
||||
if err := user.CanBeSeenBy(currentUser); err != nil {
|
||||
log.Error("%s can not be seen by viewer %s: %s", user.Username, currentUser.Username, err)
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
@ -114,12 +109,6 @@ func Profile() http.HandlerFunc {
|
|||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"NoteCount": models.CountNotesAboutUser(currentUser, user),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"ForumThreadCount": models.CountThreadsByUser(user),
|
||||
"ForumReplyCount": models.CountCommentsByUser(user, "threads"),
|
||||
"PhotoCommentCount": models.CountCommentsByUser(user, "photos"),
|
||||
"CommentsReceivedCount": models.CountCommentsReceived(user),
|
||||
"LikesGivenCount": models.CountLikesGiven(user),
|
||||
"LikesReceivedCount": models.CountLikesReceived(user),
|
||||
"OnChat": worker.GetChatStatistics().IsOnline(user.Username),
|
||||
|
||||
// Details on who likes their profile page.
|
||||
|
|
80
pkg/controller/htmx/profile_activity.go
Normal file
80
pkg/controller/htmx/profile_activity.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package htmx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/log"
|
||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||
"code.nonshy.com/nonshy/website/pkg/models"
|
||||
"code.nonshy.com/nonshy/website/pkg/session"
|
||||
"code.nonshy.com/nonshy/website/pkg/templates"
|
||||
)
|
||||
|
||||
// Statistics and social activity on the user's profile page.
|
||||
func UserProfileActivityCard() http.HandlerFunc {
|
||||
tmpl := templates.MustLoadCustom("partials/htmx/profile_activity.html")
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
username = r.FormValue("username")
|
||||
)
|
||||
|
||||
if username == "" {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Debug: use ?delay=true to force a slower response.
|
||||
if r.FormValue("delay") != "" {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// Find this user.
|
||||
user, err := models.FindUser(username)
|
||||
if err != nil {
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the current user.
|
||||
currentUser, err := session.CurrentUser(r)
|
||||
if err != nil {
|
||||
session.FlashError(w, r, "You must be signed in to view this page.")
|
||||
templates.Redirect(w, "/login?next=/u/"+url.QueryEscape(r.URL.String()))
|
||||
return
|
||||
}
|
||||
|
||||
// Is the site under a Maintenance Mode restriction?
|
||||
if middleware.MaintenanceMode(currentUser, w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Inject relationship booleans for profile picture display.
|
||||
models.SetUserRelationships(currentUser, []*models.User{user})
|
||||
|
||||
// Give a Not Found page if we can not see this user.
|
||||
if err := user.CanBeSeenBy(currentUser); err != nil {
|
||||
log.Error("%s can not be seen by viewer %s: %s", user.Username, currentUser.Username, err)
|
||||
templates.NotFoundPage(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
vars := map[string]interface{}{
|
||||
"User": user,
|
||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||
"FriendCount": models.CountFriends(user.ID),
|
||||
"ForumThreadCount": models.CountThreadsByUser(user),
|
||||
"ForumReplyCount": models.CountCommentsByUser(user, "threads"),
|
||||
"PhotoCommentCount": models.CountCommentsByUser(user, "photos"),
|
||||
"CommentsReceivedCount": models.CountCommentsReceived(user),
|
||||
"LikesGivenCount": models.CountLikesGiven(user),
|
||||
"LikesReceivedCount": models.CountLikesReceived(user),
|
||||
}
|
||||
|
||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
|
@ -226,6 +226,31 @@ func (u *User) IsShyFrom(other *User) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// CanBeSeenBy checks whether the user can be seen to exist by the viewer.
|
||||
//
|
||||
// An admin viewer can always see them, but a user may be hidden to others when they are
|
||||
// blocking, disabled or banned.
|
||||
//
|
||||
// The user should always be given a Not Found page so they can't tell the user even
|
||||
// exists. The returned error will include a specific reason, for debugging purposes.
|
||||
func (u *User) CanBeSeenBy(viewer *User) error {
|
||||
if viewer.IsAdmin {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Banned or disabled? Only admin can view then.
|
||||
if u.Status != UserStatusActive {
|
||||
return fmt.Errorf("user status is %s", u.Status)
|
||||
}
|
||||
|
||||
// Is either one blocking?
|
||||
if IsBlocking(viewer.ID, u.ID) && !viewer.IsAdmin {
|
||||
return fmt.Errorf("users block each other")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserSearch config.
|
||||
type UserSearch struct {
|
||||
Username string
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"code.nonshy.com/nonshy/website/pkg/controller/comment"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/forum"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/friend"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/htmx"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/inbox"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/index"
|
||||
"code.nonshy.com/nonshy/website/pkg/controller/photo"
|
||||
|
@ -116,6 +117,9 @@ func New() http.Handler {
|
|||
mux.Handle("POST /v1/barertc/report", barertc.Report())
|
||||
mux.Handle("POST /v1/barertc/profile", barertc.Profile())
|
||||
|
||||
// HTMX endpoints.
|
||||
mux.Handle("GET /htmx/user/profile/activity", middleware.LoginRequired(htmx.UserProfileActivityCard()))
|
||||
|
||||
// Redirect endpoints.
|
||||
mux.Handle("GET /go/comment", middleware.LoginRequired(comment.GoToComment()))
|
||||
|
||||
|
|
|
@ -46,6 +46,28 @@ func LoadTemplate(filename string) (*Template, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// LoadCustom loads a bare template without the site theme and partial templates attached.
|
||||
//
|
||||
// The custom TempleFuncs and vars are still available (PrettyTitle, .CurrentUser, etc.)
|
||||
func LoadCustom(filename string) (*Template, error) {
|
||||
filepath := config.TemplatePath + "/" + filename
|
||||
stat, err := os.Stat(filepath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadTemplate(%s): %s", filename, err)
|
||||
}
|
||||
|
||||
tmpl := template.New("page")
|
||||
tmpl.Funcs(TemplateFuncs(nil))
|
||||
tmpl.ParseFiles(filepath)
|
||||
|
||||
return &Template{
|
||||
filename: filename,
|
||||
filepath: filepath,
|
||||
modified: stat.ModTime(),
|
||||
tmpl: tmpl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Must LoadTemplate or panic.
|
||||
func Must(filename string) *Template {
|
||||
tmpl, err := LoadTemplate(filename)
|
||||
|
@ -55,6 +77,15 @@ func Must(filename string) *Template {
|
|||
return tmpl
|
||||
}
|
||||
|
||||
// Must LoadCustom or panic.
|
||||
func MustLoadCustom(filename string) *Template {
|
||||
tmpl, err := LoadCustom(filename)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tmpl
|
||||
}
|
||||
|
||||
// Execute a loaded template. In debug mode, the template file may be reloaded
|
||||
// from disk if the file on disk has been modified.
|
||||
func (t *Template) Execute(w http.ResponseWriter, r *http.Request, vars map[string]interface{}) error {
|
||||
|
|
1
web/static/js/htmx-1.9.12.min.js
vendored
Normal file
1
web/static/js/htmx-1.9.12.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -451,88 +451,12 @@
|
|||
</header>
|
||||
|
||||
<div class="card-content">
|
||||
<strong class="has-text-info">Statistics</strong>
|
||||
<table class="table is-fullwidth" style="font-size: small">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<strong class="is-size-7">Photos shared:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/u/{{.User.Username}}/photos" class="has-text-info">
|
||||
<i class="fa fa-image mr-1"></i>
|
||||
{{FormatNumberCommas .PhotoCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Forum threads written:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/forum/search?username={{.User.Username}}&type=threads" class="has-text-info">
|
||||
<i class="fa fa-comment mr-1"></i>
|
||||
{{FormatNumberCommas .ForumThreadCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Forum comments:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/forum/search?username={{.User.Username}}" class="has-text-info">
|
||||
<i class="fa fa-comments mr-1"></i>
|
||||
{{FormatNumberCommas .ForumReplyCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<strong class="has-text-info">Social</strong>
|
||||
<table class="table is-fullwidth" style="font-size: small">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<strong class="is-size-7">Friends:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/u/{{.User.Username}}/friends" class="has-text-info">
|
||||
<i class="fa fa-user-group mr-1"></i> {{FormatNumberCommas .FriendCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Photo comments written:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .PhotoCommentCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Photo comments received:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .CommentsReceivedCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Likes given:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesGivenCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Likes received:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesReceivedCount}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Lazy load the statistics card-->
|
||||
<div hx-get="/htmx/user/profile/activity?username={{.User.Username}}&delay=1" hx-trigger="load">
|
||||
<i class="fa fa-spinner fa-spin mr-1"></i> Loading...
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -383,6 +383,7 @@
|
|||
<script type="text/javascript" src="/static/js/bulma.js?build={{.BuildHash}}"></script>
|
||||
<script type="text/javascript" src="/static/js/likes.js?build={{.BuildHash}}"></script>
|
||||
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
||||
<script type="text/javascript" src="/static/js/htmx-1.9.12.min.js"></script>
|
||||
{{template "scripts" .}}
|
||||
|
||||
<!-- Likes modal -->
|
||||
|
|
84
web/templates/partials/htmx/profile_activity.html
Normal file
84
web/templates/partials/htmx/profile_activity.html
Normal file
|
@ -0,0 +1,84 @@
|
|||
{{define "base"}}
|
||||
<strong class="has-text-info">Statistics</strong>
|
||||
<table class="table is-fullwidth" style="font-size: small">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<strong class="is-size-7">Photos shared:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/u/{{.User.Username}}/photos" class="has-text-info">
|
||||
<i class="fa fa-image mr-1"></i>
|
||||
{{FormatNumberCommas .PhotoCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Forum threads written:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/forum/search?username={{.User.Username}}&type=threads" class="has-text-info">
|
||||
<i class="fa fa-comment mr-1"></i>
|
||||
{{FormatNumberCommas .ForumThreadCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Forum comments:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/forum/search?username={{.User.Username}}" class="has-text-info">
|
||||
<i class="fa fa-comments mr-1"></i>
|
||||
{{FormatNumberCommas .ForumReplyCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<strong class="has-text-info">Social</strong>
|
||||
<table class="table is-fullwidth" style="font-size: small">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<strong class="is-size-7">Friends:</label>
|
||||
</td>
|
||||
<td>
|
||||
<a href="/u/{{.User.Username}}/friends" class="has-text-info">
|
||||
<i class="fa fa-user-group mr-1"></i> {{FormatNumberCommas .FriendCount}}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Photo comments written:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .PhotoCommentCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Photo comments received:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-comments mr-1"></i> {{FormatNumberCommas .CommentsReceivedCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Likes given:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesGivenCount}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<strong class="is-size-7">Likes received:</label>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-heart mr-1"></i> {{FormatNumberCommas .LikesReceivedCount}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{end}}
|
Loading…
Reference in New Issue
Block a user