See who has "Liked" something
This commit is contained in:
parent
3543dd3e42
commit
de30f5e952
|
@ -23,4 +23,5 @@ var (
|
||||||
PageSizeThreadList = 20 // 20 threads per board, 20 posts per thread
|
PageSizeThreadList = 20 // 20 threads per board, 20 posts per thread
|
||||||
PageSizeForumAdmin = 20
|
PageSizeForumAdmin = 20
|
||||||
PageSizeDashboardNotifications = 50
|
PageSizeDashboardNotifications = 50
|
||||||
|
PageSizeLikeList = 12 // number of likes to show in popup modal
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
"code.nonshy.com/nonshy/website/pkg/middleware"
|
"code.nonshy.com/nonshy/website/pkg/middleware"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
|
@ -105,12 +106,24 @@ func Profile() http.HandlerFunc {
|
||||||
// Get Likes for this profile.
|
// Get Likes for this profile.
|
||||||
likeMap := models.MapLikes(currentUser, "users", []uint64{user.ID})
|
likeMap := models.MapLikes(currentUser, "users", []uint64{user.ID})
|
||||||
|
|
||||||
|
// Get the summary of WHO liked this picture.
|
||||||
|
likeExample, likeRemainder, err := models.WhoLikes("users", user.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("WhoLikes(user %d): %s", user.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
vars := map[string]interface{}{
|
vars := map[string]interface{}{
|
||||||
"User": user,
|
"User": user,
|
||||||
"LikeMap": likeMap,
|
"LikeMap": likeMap,
|
||||||
"IsFriend": isFriend,
|
"IsFriend": isFriend,
|
||||||
"IsPrivate": isPrivate,
|
"IsPrivate": isPrivate,
|
||||||
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
"PhotoCount": models.CountPhotosICanSee(user, currentUser),
|
||||||
|
|
||||||
|
// Details on who likes the photo.
|
||||||
|
"LikeExample": likeExample,
|
||||||
|
"LikeRemainder": likeRemainder,
|
||||||
|
"LikeTableName": "users",
|
||||||
|
"LikeTableID": user.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
|
@ -3,13 +3,15 @@ package api
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
"code.nonshy.com/nonshy/website/pkg/log"
|
"code.nonshy.com/nonshy/website/pkg/log"
|
||||||
"code.nonshy.com/nonshy/website/pkg/models"
|
"code.nonshy.com/nonshy/website/pkg/models"
|
||||||
"code.nonshy.com/nonshy/website/pkg/session"
|
"code.nonshy.com/nonshy/website/pkg/session"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Likes API.
|
// Likes API posts a new like on something.
|
||||||
func Likes() http.HandlerFunc {
|
func Likes() http.HandlerFunc {
|
||||||
// Request JSON schema.
|
// Request JSON schema.
|
||||||
type Request struct {
|
type Request struct {
|
||||||
|
@ -70,11 +72,11 @@ func Likes() http.HandlerFunc {
|
||||||
if user, err := models.GetUser(photo.UserID); err == nil {
|
if user, err := models.GetUser(photo.UserID); err == nil {
|
||||||
// Admin safety check: in case the admin clicked 'Like' on a friends-only or private
|
// Admin safety check: in case the admin clicked 'Like' on a friends-only or private
|
||||||
// picture they shouldn't have been expected to see, do not log a like.
|
// picture they shouldn't have been expected to see, do not log a like.
|
||||||
if currentUser.IsAdmin {
|
if currentUser.IsAdmin && currentUser.ID != user.ID {
|
||||||
if (photo.Visibility == models.PhotoFriends && !models.AreFriends(user.ID, currentUser.ID)) ||
|
if (photo.Visibility == models.PhotoFriends && !models.AreFriends(user.ID, currentUser.ID)) ||
|
||||||
(photo.Visibility == models.PhotoPrivate && !models.IsPrivateUnlocked(user.ID, currentUser.ID)) {
|
(photo.Visibility == models.PhotoPrivate && !models.IsPrivateUnlocked(user.ID, currentUser.ID)) {
|
||||||
SendJSON(w, http.StatusForbidden, Response{
|
SendJSON(w, http.StatusForbidden, Response{
|
||||||
Error: fmt.Sprintf("You are not allowed to like that photo."),
|
Error: "You are not allowed to like that photo.",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -155,3 +157,92 @@ func Likes() http.HandlerFunc {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WhoLikes API checks who liked something.
|
||||||
|
func WhoLikes() http.HandlerFunc {
|
||||||
|
// Response JSON schema.
|
||||||
|
type Liker struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Avatar string `json:"avatar"`
|
||||||
|
Relationship models.UserRelationship `json:"relationship"`
|
||||||
|
}
|
||||||
|
type Response struct {
|
||||||
|
OK bool `json:"OK"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Likes []Liker `json:"likes,omitempty"`
|
||||||
|
Pager *models.Pagination `json:"pager,omitempty"`
|
||||||
|
Pages int `json:"pages,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
SendJSON(w, http.StatusNotAcceptable, Response{
|
||||||
|
Error: "GET method only",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse request parameters.
|
||||||
|
var (
|
||||||
|
tableName = r.FormValue("table_name")
|
||||||
|
tableID, _ = strconv.Atoi(r.FormValue("table_id"))
|
||||||
|
page, _ = strconv.Atoi(r.FormValue("page"))
|
||||||
|
)
|
||||||
|
if tableName == "" {
|
||||||
|
SendJSON(w, http.StatusBadRequest, Response{
|
||||||
|
Error: "Missing required table_name",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if tableID == 0 {
|
||||||
|
SendJSON(w, http.StatusBadRequest, Response{
|
||||||
|
Error: "Missing required table_id",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if page < 1 {
|
||||||
|
page = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
SendJSON(w, http.StatusBadRequest, Response{
|
||||||
|
Error: "Couldn't get current user!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a page of users who've liked this.
|
||||||
|
var pager = &models.Pagination{
|
||||||
|
Page: page,
|
||||||
|
PerPage: config.PageSizeLikeList,
|
||||||
|
Sort: "created_at desc",
|
||||||
|
}
|
||||||
|
users, err := models.PaginateLikes(currentUser, tableName, uint64(tableID), pager)
|
||||||
|
if err != nil {
|
||||||
|
SendJSON(w, http.StatusInternalServerError, Response{
|
||||||
|
Error: fmt.Sprintf("Error getting likes: %s", err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map user data to just the essentials for front-end.
|
||||||
|
var result = []Liker{}
|
||||||
|
for _, user := range users {
|
||||||
|
result = append(result, Liker{
|
||||||
|
Username: user.Username,
|
||||||
|
Avatar: user.VisibleAvatarURL(currentUser),
|
||||||
|
Relationship: user.UserRelationship,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send success response.
|
||||||
|
SendJSON(w, http.StatusOK, Response{
|
||||||
|
OK: true,
|
||||||
|
Likes: result,
|
||||||
|
Pager: pager,
|
||||||
|
Pages: pager.Pages(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -96,6 +96,12 @@ func View() http.HandlerFunc {
|
||||||
}
|
}
|
||||||
commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs)
|
commentLikeMap := models.MapLikes(currentUser, "comments", commentIDs)
|
||||||
|
|
||||||
|
// Get the summary of WHO liked this picture.
|
||||||
|
likeExample, likeRemainder, err := models.WhoLikes("photos", photo.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("WhoLikes(photo %d): %s", photo.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Populate the user relationships in these comments.
|
// Populate the user relationships in these comments.
|
||||||
models.SetUserRelationshipsInComments(currentUser, comments)
|
models.SetUserRelationshipsInComments(currentUser, comments)
|
||||||
|
|
||||||
|
@ -111,6 +117,12 @@ func View() http.HandlerFunc {
|
||||||
"Comments": comments,
|
"Comments": comments,
|
||||||
"CommentLikeMap": commentLikeMap,
|
"CommentLikeMap": commentLikeMap,
|
||||||
"IsSubscribed": isSubscribed,
|
"IsSubscribed": isSubscribed,
|
||||||
|
|
||||||
|
// Details on who likes the photo.
|
||||||
|
"LikeExample": likeExample,
|
||||||
|
"LikeRemainder": likeRemainder,
|
||||||
|
"LikeTableName": "photos",
|
||||||
|
"LikeTableID": photo.ID,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
|
@ -63,6 +63,68 @@ func CountLikes(tableName string, tableID uint64) int64 {
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WhoLikes something. Returns the first couple users and a count of the remainder.
|
||||||
|
func WhoLikes(tableName string, tableID uint64) ([]*User, int64, error) {
|
||||||
|
var (
|
||||||
|
userIDs = []uint64{}
|
||||||
|
likes = []*Like{}
|
||||||
|
res = DB.Model(&Like{}).Where(
|
||||||
|
"table_name = ? AND table_id = ?",
|
||||||
|
tableName, tableID,
|
||||||
|
).Order("created_at DESC").Limit(2).Scan(&likes)
|
||||||
|
total = CountLikes(tableName, tableID)
|
||||||
|
remainder = total - int64(len(likes))
|
||||||
|
)
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, 0, res.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect the user IDs to look up.
|
||||||
|
for _, row := range likes {
|
||||||
|
userIDs = append(userIDs, row.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the users and return the remainder.
|
||||||
|
users, err := GetUsers(nil, userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, remainder, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateLikes returns a paged view of users who've liked something.
|
||||||
|
func PaginateLikes(currentUser *User, tableName string, tableID uint64, pager *Pagination) ([]*User, error) {
|
||||||
|
var (
|
||||||
|
l = []*Like{}
|
||||||
|
userIDs = []uint64{}
|
||||||
|
)
|
||||||
|
|
||||||
|
query := DB.Where(
|
||||||
|
"table_name = ? AND table_id = ?",
|
||||||
|
tableName, tableID,
|
||||||
|
).Order(
|
||||||
|
pager.Sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the total count.
|
||||||
|
query.Model(&Like{}).Count(&pager.Total)
|
||||||
|
|
||||||
|
// Get the page of likes.
|
||||||
|
result := query.Offset(
|
||||||
|
pager.GetOffset(),
|
||||||
|
).Limit(pager.PerPage).Find(&l)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the user IDs in.
|
||||||
|
for _, like := range l {
|
||||||
|
userIDs = append(userIDs, like.UserID)
|
||||||
|
}
|
||||||
|
return GetUsers(currentUser, userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
// LikedIDs filters a set of table IDs to ones the user likes.
|
// LikedIDs filters a set of table IDs to ones the user likes.
|
||||||
func LikedIDs(user *User, tableName string, tableIDs []uint64) ([]uint64, error) {
|
func LikedIDs(user *User, tableName string, tableIDs []uint64) ([]uint64, error) {
|
||||||
var result = []uint64{}
|
var result = []uint64{}
|
||||||
|
|
|
@ -452,6 +452,23 @@ func (u *User) NameOrUsername() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VisibleAvatarURL returns a URL to the user's avatar taking into account
|
||||||
|
// their relationship with the current user. For example, if the avatar is
|
||||||
|
// friends-only and the current user can't see it, returns the path to the
|
||||||
|
// yellow placeholder avatar instead.
|
||||||
|
//
|
||||||
|
// Expects that UserRelationships are available on the user.
|
||||||
|
func (u *User) VisibleAvatarURL(currentUser *User) string {
|
||||||
|
if u.ProfilePhoto.Visibility == PhotoPrivate && !u.UserRelationship.IsPrivateGranted {
|
||||||
|
return "/static/img/shy-private.png"
|
||||||
|
} else if u.ProfilePhoto.Visibility == PhotoFriends && !u.UserRelationship.IsFriend {
|
||||||
|
return "/static/img/shy-friends.png"
|
||||||
|
} else if u.ProfilePhoto.CroppedFilename != "" {
|
||||||
|
return config.PhotoWebPath + "/" + u.ProfilePhoto.CroppedFilename
|
||||||
|
}
|
||||||
|
return "/static/img/shy.png"
|
||||||
|
}
|
||||||
|
|
||||||
// HashPassword sets the user's hashed (bcrypt) password.
|
// HashPassword sets the user's hashed (bcrypt) password.
|
||||||
func (u *User) HashPassword(password string) error {
|
func (u *User) HashPassword(password string) error {
|
||||||
passwd, err := bcrypt.GenerateFromPassword([]byte(password), config.BcryptCost)
|
passwd, err := bcrypt.GenerateFromPassword([]byte(password), config.BcryptCost)
|
||||||
|
|
|
@ -97,6 +97,7 @@ func New() http.Handler {
|
||||||
mux.HandleFunc("/v1/users/me", api.LoginOK())
|
mux.HandleFunc("/v1/users/me", api.LoginOK())
|
||||||
mux.HandleFunc("/v1/users/check-username", api.UsernameCheck())
|
mux.HandleFunc("/v1/users/check-username", api.UsernameCheck())
|
||||||
mux.Handle("/v1/likes", middleware.LoginRequired(api.Likes()))
|
mux.Handle("/v1/likes", middleware.LoginRequired(api.Likes()))
|
||||||
|
mux.Handle("/v1/likes/users", middleware.LoginRequired(api.WhoLikes()))
|
||||||
mux.Handle("/v1/notifications/read", middleware.LoginRequired(api.ReadNotification()))
|
mux.Handle("/v1/notifications/read", middleware.LoginRequired(api.ReadNotification()))
|
||||||
mux.Handle("/v1/notifications/delete", middleware.LoginRequired(api.ClearNotification()))
|
mux.Handle("/v1/notifications/delete", middleware.LoginRequired(api.ClearNotification()))
|
||||||
mux.Handle("/v1/comment-photos/remove-orphaned", api.RemoveOrphanedCommentPhotos())
|
mux.Handle("/v1/comment-photos/remove-orphaned", api.RemoveOrphanedCommentPhotos())
|
||||||
|
|
|
@ -119,6 +119,7 @@ func (t *Template) Reload() error {
|
||||||
var baseTemplates = []string{
|
var baseTemplates = []string{
|
||||||
config.TemplatePath + "/base.html",
|
config.TemplatePath + "/base.html",
|
||||||
config.TemplatePath + "/partials/user_avatar.html",
|
config.TemplatePath + "/partials/user_avatar.html",
|
||||||
|
config.TemplatePath + "/partials/like_modal.html",
|
||||||
}
|
}
|
||||||
|
|
||||||
// templates returns a template chain with the base templates preceding yours.
|
// templates returns a template chain with the base templates preceding yours.
|
||||||
|
|
|
@ -85,6 +85,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Common event handlers for bulma modals.
|
// Common event handlers for bulma modals.
|
||||||
(document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => {
|
(document.querySelectorAll(".modal-background, .modal-close, .photo-modal") || []).forEach(node => {
|
||||||
const target = node.closest(".modal");
|
const target = node.closest(".modal");
|
||||||
|
if (target.classList.contains("vue-managed")) return;
|
||||||
node.addEventListener("click", () => {
|
node.addEventListener("click", () => {
|
||||||
target.classList.remove("is-active");
|
target.classList.remove("is-active");
|
||||||
});
|
});
|
||||||
|
|
|
@ -256,7 +256,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="block p-4">
|
<div class="block p-4">
|
||||||
<div class="tabs is-boxed">
|
<div class="tabs is-boxed mb-0">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
<a>
|
<a>
|
||||||
|
@ -280,7 +280,12 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="columns">
|
<!-- Show who likes this user -->
|
||||||
|
{{if not .CurrentUser.IsShy}}
|
||||||
|
{{template "like-example" .}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="columns mt-1">
|
||||||
|
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
|
|
|
@ -334,6 +334,9 @@
|
||||||
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
<script type="text/javascript" src="/static/js/vue-3.2.45.js"></script>
|
||||||
{{template "scripts" .}}
|
{{template "scripts" .}}
|
||||||
|
|
||||||
|
<!-- Likes modal -->
|
||||||
|
{{template "like-modal"}}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -251,6 +251,15 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<!-- Button to inspect the likes -->
|
||||||
|
<a href="#" class="has-text-dark"
|
||||||
|
onclick="ShowLikeModal('comments', {{.ID}}); return false">
|
||||||
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||||
|
<span>Likes</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="/contact?intent=report&subject=report.comment&id={{.ID}}" class="has-text-dark">
|
<a href="/contact?intent=report&subject=report.comment&id={{.ID}}" class="has-text-dark">
|
||||||
<span class="icon"><i class="fa fa-flag"></i></span>
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
|
209
web/templates/partials/like_modal.html
Normal file
209
web/templates/partials/like_modal.html
Normal file
|
@ -0,0 +1,209 @@
|
||||||
|
<!-- "Likes" modal to see who liked a thing -->
|
||||||
|
|
||||||
|
<!-- Reusable "Liked by Alice, Bob and 5 others" widget that invokes
|
||||||
|
the Like Modal. Requirements: pass in a context providing the
|
||||||
|
following variables:
|
||||||
|
|
||||||
|
- LikeExample: slice of users
|
||||||
|
- LikeRemainder: integer
|
||||||
|
- LikeTableName: like 'photos'
|
||||||
|
- LikeTableID: integer
|
||||||
|
|
||||||
|
Call this like: {{template "like-example" .}}
|
||||||
|
-->
|
||||||
|
{{define "like-example"}}
|
||||||
|
{{$Outer := .}}
|
||||||
|
{{if .LikeExample}}
|
||||||
|
<div class="mt-4 mb-2 has-text-centered">
|
||||||
|
Liked by
|
||||||
|
|
||||||
|
<!-- User list -->
|
||||||
|
{{range $i, $User := .LikeExample}}
|
||||||
|
<!-- Avatar -->
|
||||||
|
<figure class="image is-16x16 is-inline-block">
|
||||||
|
<a href="/u/{{$User.Username}}" class="has-text-dark">
|
||||||
|
{{if $User.ProfilePhoto.ID}}
|
||||||
|
{{if and (eq $User.ProfilePhoto.Visibility "private") (not $User.UserRelationship.IsPrivateGranted)}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy-private.png">
|
||||||
|
{{else if and (eq $User.ProfilePhoto.Visibility "friends") (not $User.UserRelationship.IsFriend)}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy-friends.png">
|
||||||
|
{{else}}
|
||||||
|
<img class="is-rounded" src="{{PhotoURL $User.ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{end}}
|
||||||
|
{{else}}
|
||||||
|
<img class="is-rounded" src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- Username plus a comma if multiple -->
|
||||||
|
<a href="/u/{{$User.Username}}">
|
||||||
|
{{- $User.Username -}}
|
||||||
|
</a>{{if and (eq (len $Outer.LikeExample) 2) (not $Outer.LikeRemainder)}}
|
||||||
|
{{if eq $i 0}}
|
||||||
|
and
|
||||||
|
{{end}}
|
||||||
|
{{- else if gt (len $Outer.LikeExample) 1}},{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<!-- Others -->
|
||||||
|
{{if .LikeRemainder}}
|
||||||
|
and
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
onclick="ShowLikeModal('{{.LikeTableName}}', {{.LikeTableID}}); return false"
|
||||||
|
>
|
||||||
|
{{.LikeRemainder}} other{{Pluralize64 .LikeRemainder}}
|
||||||
|
</a>.
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "like-modal"}}
|
||||||
|
<div id="like-modal-app">
|
||||||
|
<div class="modal vue-managed" :class="{'is-active': visible}">
|
||||||
|
<div class="modal-background" @click="visible=false"></div>
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header has-background-info">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<i class="fa fa-heart mr-2"></i> [[title]]
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<div v-if="busy">
|
||||||
|
<i class="fa fa-spinner fa-spin"></i> Loading likes...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<p class="block">
|
||||||
|
Found [[ total ]] like[[ total === 1 ? '' : 's' ]]
|
||||||
|
(page [[ page ]] of [[ pages ]]).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
<div class="column is-one-third"
|
||||||
|
v-for="row in result">
|
||||||
|
|
||||||
|
<!-- Avatar -->
|
||||||
|
<figure class="image is-16x16 is-inline-block mr-2">
|
||||||
|
<a :href="'/u/'+row.username" class="has-text-dark">
|
||||||
|
<img class="is-rounded" :src="row.avatar">
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
|
||||||
|
<!-- Username link -->
|
||||||
|
<a :href="'/u/'+row.username" target="_blank">[[ row.username ]]</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pager buttons -->
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button type="button" class="button"
|
||||||
|
@click="prevPage()"
|
||||||
|
:disabled="page <= 1">
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button type="button" class="button"
|
||||||
|
@click="nextPage()"
|
||||||
|
:disabled="page >= pages">
|
||||||
|
Next page
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="column has-text-right">
|
||||||
|
<button type="button" class="button is-primary"
|
||||||
|
@click="visible=false">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Implemented in Vue widget.
|
||||||
|
var ShowLikeModal = function(tableName, tableID) {};
|
||||||
|
|
||||||
|
const likeModalApp = Vue.createApp({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
title: "Likes",
|
||||||
|
|
||||||
|
busy: false,
|
||||||
|
tableName: "photos",
|
||||||
|
tableID: 0,
|
||||||
|
page: 1,
|
||||||
|
pages: 0,
|
||||||
|
total: 0,
|
||||||
|
result: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
ShowLikeModal = this.ShowLikeModal;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
ShowLikeModal(tableName, tableID) {
|
||||||
|
this.tableName = tableName;
|
||||||
|
this.tableID = tableID;
|
||||||
|
this.visible = true;
|
||||||
|
this.page = 1;
|
||||||
|
this.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
this.page--;
|
||||||
|
if (this.page <= 1) {
|
||||||
|
this.page = 1;
|
||||||
|
}
|
||||||
|
this.get();
|
||||||
|
},
|
||||||
|
nextPage() {
|
||||||
|
this.page++;
|
||||||
|
this.get();
|
||||||
|
},
|
||||||
|
|
||||||
|
get() {
|
||||||
|
this.busy = true;
|
||||||
|
return fetch("/v1/likes/users?" + new URLSearchParams({
|
||||||
|
table_name: this.tableName,
|
||||||
|
table_id: this.tableID,
|
||||||
|
page: this.page,
|
||||||
|
}), {
|
||||||
|
method: "GET",
|
||||||
|
mode: "same-origin",
|
||||||
|
cache: "no-cache",
|
||||||
|
credentials: "same-origin",
|
||||||
|
})
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.StatusCode !== 200) {
|
||||||
|
window.alert(data.data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let likes = data.data.likes;
|
||||||
|
this.pages = data.data.pages;
|
||||||
|
this.total = data.data.pager.Total;
|
||||||
|
this.result = likes;
|
||||||
|
}).catch(resp => {
|
||||||
|
window.alert(resp);
|
||||||
|
}).finally(() => {
|
||||||
|
this.busy = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
likeModalApp.mount("#like-modal-app");
|
||||||
|
</script>
|
||||||
|
{{end}}
|
|
@ -84,6 +84,9 @@
|
||||||
{{.Photo.Caption}}
|
{{.Photo.Caption}}
|
||||||
{{else}}<em>No caption</em>{{end}}
|
{{else}}<em>No caption</em>{{end}}
|
||||||
|
|
||||||
|
<!-- Who likes this photo? -->
|
||||||
|
{{template "like-example" .}}
|
||||||
|
|
||||||
<!-- Like & Comments buttons -->
|
<!-- Like & Comments buttons -->
|
||||||
<div class="mt-4 mb-2 columns is-centered is-mobile is-gapless">
|
<div class="mt-4 mb-2 columns is-centered is-mobile is-gapless">
|
||||||
<div class="column is-narrow mr-2">
|
<div class="column is-narrow mr-2">
|
||||||
|
@ -249,6 +252,15 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<!-- Button to inspect the likes -->
|
||||||
|
<a href="#" class="has-text-dark"
|
||||||
|
onclick="ShowLikeModal('comments', {{.ID}}); return false">
|
||||||
|
<span class="icon"><i class="fa fa-eye"></i></span>
|
||||||
|
<span>Likes</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
<a href="/contact?intent=report&subject=report.comment&id={{.ID}}" class="has-text-dark">
|
<a href="/contact?intent=report&subject=report.comment&id={{.ID}}" class="has-text-dark">
|
||||||
<span class="icon"><i class="fa fa-flag"></i></span>
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user