Friend Requests and User Search
This commit is contained in:
parent
c8f6cf2e4d
commit
4ad7e00c44
12
pkg/config/page_sizes.go
Normal file
12
pkg/config/page_sizes.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
// Pagination sizes per page.
|
||||||
|
var (
|
||||||
|
PageSizeMemberSearch = 60
|
||||||
|
PageSizeFriends = 12
|
||||||
|
PageSizeAdminCertification = 20
|
||||||
|
PageSizeSiteGallery = 18
|
||||||
|
PageSizeUserGallery = 18
|
||||||
|
PageSizeInboxList = 20 // sidebar list
|
||||||
|
PageSizeInboxThread = 20 // conversation view
|
||||||
|
)
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +22,14 @@ func Profile() http.HandlerFunc {
|
||||||
username = m[1]
|
username = m[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Find this user.
|
// Find this user.
|
||||||
user, err := models.FindUser(username)
|
user, err := models.FindUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -30,6 +39,7 @@ func Profile() http.HandlerFunc {
|
||||||
|
|
||||||
vars := map[string]interface{}{
|
vars := map[string]interface{}{
|
||||||
"User": user,
|
"User": user,
|
||||||
|
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
64
pkg/controller/account/search.go
Normal file
64
pkg/controller/account/search.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package account
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Search controller.
|
||||||
|
func Search() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("account/search.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Search filters.
|
||||||
|
var (
|
||||||
|
isCertified = r.FormValue("certified")
|
||||||
|
username = r.FormValue("username") // email or username
|
||||||
|
gender = r.FormValue("gender")
|
||||||
|
orientation = r.FormValue("orientation")
|
||||||
|
maritalStatus = r.FormValue("marital_status")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Default
|
||||||
|
if isCertified == "" {
|
||||||
|
isCertified = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
pager := &models.Pagination{
|
||||||
|
PerPage: config.PageSizeMemberSearch,
|
||||||
|
}
|
||||||
|
pager.ParsePage(r)
|
||||||
|
|
||||||
|
users, err := models.SearchUsers(&models.UserSearch{
|
||||||
|
EmailOrUsername: username,
|
||||||
|
Gender: gender,
|
||||||
|
Orientation: orientation,
|
||||||
|
MaritalStatus: maritalStatus,
|
||||||
|
Certified: isCertified == "true",
|
||||||
|
}, pager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't search users: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"Users": users,
|
||||||
|
"Pager": pager,
|
||||||
|
"Enum": config.ProfileEnums,
|
||||||
|
|
||||||
|
// Search filter values.
|
||||||
|
"Certified": isCertified,
|
||||||
|
"Gender": gender,
|
||||||
|
"Orientation": orientation,
|
||||||
|
"MaritalStatus": maritalStatus,
|
||||||
|
"EmailOrUsername": username,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
48
pkg/controller/friend/friends.go
Normal file
48
pkg/controller/friend/friends.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package friend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Friends list and pending friend request endpoint.
|
||||||
|
func Friends() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("friend/friends.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
isRequests := r.FormValue("view") == "requests"
|
||||||
|
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Unexpected error: could not get currentUser.")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get our friends.
|
||||||
|
pager := &models.Pagination{
|
||||||
|
PerPage: config.PageSizeFriends,
|
||||||
|
Sort: "updated_at desc",
|
||||||
|
}
|
||||||
|
pager.ParsePage(r)
|
||||||
|
friends, err := models.PaginateFriends(currentUser.ID, isRequests, pager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't paginate friends: %s", err)
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"IsRequests": isRequests,
|
||||||
|
"Friends": friends,
|
||||||
|
"Pager": pager,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
89
pkg/controller/friend/request.go
Normal file
89
pkg/controller/friend/request.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package friend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddFriend controller to send a friend request.
|
||||||
|
func AddFriend() http.HandlerFunc {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// POST only.
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
session.FlashError(w, r, "Unacceptable Request Method")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
var (
|
||||||
|
username = strings.ToLower(r.PostFormValue("username"))
|
||||||
|
verdict = r.PostFormValue("verdict")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get the current user.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get CurrentUser: %s", err)
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the target user.
|
||||||
|
user, err := models.FindUser(username)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "User Not Found")
|
||||||
|
templates.Redirect(w, "/")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't friend yourself.
|
||||||
|
if currentUser.ID == user.ID {
|
||||||
|
session.FlashError(w, r, "You can't send a friend request to yourself!")
|
||||||
|
templates.Redirect(w, "/u/"+username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we adding, or rejecting+removing?
|
||||||
|
if verdict == "reject" || verdict == "remove" {
|
||||||
|
err := models.RemoveFriend(currentUser.ID, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Failed to remove friend: %s", err)
|
||||||
|
templates.Redirect(w, "/u/"+username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var message string
|
||||||
|
if verdict == "reject" {
|
||||||
|
message = fmt.Sprintf("Friend request from %s has been rejected.", username)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("Removed friendship with %s.", username)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Flash(w, r, message)
|
||||||
|
if verdict == "reject" {
|
||||||
|
templates.Redirect(w, "/friends?view=requests")
|
||||||
|
}
|
||||||
|
templates.Redirect(w, "/friends")
|
||||||
|
} else {
|
||||||
|
// Post the friend request.
|
||||||
|
if err := models.AddFriend(currentUser.ID, user.ID); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't send friend request: %s.", err)
|
||||||
|
} else {
|
||||||
|
if verdict == "approve" {
|
||||||
|
session.Flash(w, r, "You accepted the friend request from %s!", username)
|
||||||
|
templates.Redirect(w, "/friends?view=requests")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session.Flash(w, r, "Friend request sent!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.Redirect(w, "/u/"+username)
|
||||||
|
})
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
@ -58,7 +59,7 @@ func Inbox() http.HandlerFunc {
|
||||||
|
|
||||||
// Get the full chat thread (paginated).
|
// Get the full chat thread (paginated).
|
||||||
threadPager = &models.Pagination{
|
threadPager = &models.Pagination{
|
||||||
PerPage: 5,
|
PerPage: config.PageSizeInboxThread,
|
||||||
Sort: "created_at desc",
|
Sort: "created_at desc",
|
||||||
}
|
}
|
||||||
threadPager.ParsePage(r)
|
threadPager.ParsePage(r)
|
||||||
|
@ -84,7 +85,7 @@ func Inbox() http.HandlerFunc {
|
||||||
// Get the inbox list of messages.
|
// Get the inbox list of messages.
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: 5,
|
PerPage: config.PageSizeInboxList,
|
||||||
Sort: "created_at desc",
|
Sort: "created_at desc",
|
||||||
}
|
}
|
||||||
if viewThread == nil {
|
if viewThread == nil {
|
||||||
|
|
|
@ -271,7 +271,7 @@ func AdminCertification() http.HandlerFunc {
|
||||||
// Get the pending photos.
|
// Get the pending photos.
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: 20,
|
PerPage: config.PageSizeAdminCertification,
|
||||||
Sort: "updated_at desc",
|
Sort: "updated_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package photo
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
@ -29,7 +30,7 @@ func SiteGallery() http.HandlerFunc {
|
||||||
// Get the page of photos.
|
// Get the page of photos.
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: 8,
|
PerPage: config.PageSizeSiteGallery,
|
||||||
Sort: "created_at desc",
|
Sort: "created_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
@ -49,6 +50,8 @@ func UserPhotos() http.HandlerFunc {
|
||||||
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
||||||
if isOwnPhotos || currentUser.IsAdmin {
|
if isOwnPhotos || currentUser.IsAdmin {
|
||||||
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
||||||
|
} else if models.AreFriends(user.ID, currentUser.ID) {
|
||||||
|
visibility = append(visibility, models.PhotoFriends)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit photo filter?
|
// Explicit photo filter?
|
||||||
|
@ -60,7 +63,7 @@ func UserPhotos() http.HandlerFunc {
|
||||||
// Get the page of photos.
|
// Get the page of photos.
|
||||||
pager := &models.Pagination{
|
pager := &models.Pagination{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PerPage: 8,
|
PerPage: config.PageSizeUserGallery,
|
||||||
Sort: "created_at desc",
|
Sort: "created_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
|
|
175
pkg/models/friend.go
Normal file
175
pkg/models/friend.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Friend table.
|
||||||
|
type Friend struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
SourceUserID uint64 `gorm:"index"`
|
||||||
|
TargetUserID uint64 `gorm:"index"`
|
||||||
|
Approved bool `gorm:"index"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFriend sends a friend request or accepts one if there was already a pending one.
|
||||||
|
func AddFriend(sourceUserID, targetUserID uint64) error {
|
||||||
|
// Did we already send a friend request?
|
||||||
|
f := &Friend{}
|
||||||
|
forward := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).First(&f).Error
|
||||||
|
|
||||||
|
// Is there a reverse friend request pending?
|
||||||
|
rev := &Friend{}
|
||||||
|
reverse := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
targetUserID, sourceUserID,
|
||||||
|
).First(&rev).Error
|
||||||
|
|
||||||
|
// If the reverse exists (requested us) but not the forward, this completes the friendship.
|
||||||
|
if reverse == nil && forward != nil {
|
||||||
|
// Approve the reverse.
|
||||||
|
rev.Approved = true
|
||||||
|
rev.Save()
|
||||||
|
|
||||||
|
// Add the matching forward.
|
||||||
|
f = &Friend{
|
||||||
|
SourceUserID: sourceUserID,
|
||||||
|
TargetUserID: targetUserID,
|
||||||
|
Approved: true,
|
||||||
|
}
|
||||||
|
return DB.Create(f).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the forward already existed, error.
|
||||||
|
if forward == nil {
|
||||||
|
if f.Approved {
|
||||||
|
return errors.New("you are already friends")
|
||||||
|
}
|
||||||
|
return errors.New("a friend request had already been sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the pending forward request.
|
||||||
|
f = &Friend{
|
||||||
|
SourceUserID: sourceUserID,
|
||||||
|
TargetUserID: targetUserID,
|
||||||
|
Approved: false,
|
||||||
|
}
|
||||||
|
return DB.Create(f).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// AreFriends quickly checks if two user IDs are friends.
|
||||||
|
func AreFriends(sourceUserID, targetUserID uint64) bool {
|
||||||
|
f := &Friend{}
|
||||||
|
DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).First(&f)
|
||||||
|
return f.Approved
|
||||||
|
}
|
||||||
|
|
||||||
|
// FriendStatus returns an indicator of friendship status: "none", "pending", "approved"
|
||||||
|
func FriendStatus(sourceUserID, targetUserID uint64) string {
|
||||||
|
f := &Friend{}
|
||||||
|
result := DB.Where(
|
||||||
|
"source_user_id = ? AND target_user_id = ?",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).First(&f)
|
||||||
|
if result.Error == nil {
|
||||||
|
if f.Approved {
|
||||||
|
return "approved"
|
||||||
|
}
|
||||||
|
return "pending"
|
||||||
|
}
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountFriendRequests gets a count of pending requests for the user.
|
||||||
|
func CountFriendRequests(userID uint64) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
result := DB.Where(
|
||||||
|
"target_user_id = ? AND approved = ?",
|
||||||
|
userID,
|
||||||
|
false,
|
||||||
|
).Model(&Friend{}).Count(&count)
|
||||||
|
return count, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateFriends gets a page of friends (or pending friend requests) as User objects ordered
|
||||||
|
// by friendship date.
|
||||||
|
func PaginateFriends(userID uint64, requests bool, pager *Pagination) ([]*User, error) {
|
||||||
|
// We paginate over the Friend table.
|
||||||
|
var (
|
||||||
|
fs = []*Friend{}
|
||||||
|
userIDs = []uint64{}
|
||||||
|
query *gorm.DB
|
||||||
|
)
|
||||||
|
|
||||||
|
if requests {
|
||||||
|
query = DB.Where(
|
||||||
|
"target_user_id = ? AND approved = ?",
|
||||||
|
userID,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
query = DB.Where(
|
||||||
|
"source_user_id = ? AND approved = ?",
|
||||||
|
userID,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = query.Order(pager.Sort)
|
||||||
|
query.Model(&Friend{}).Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&fs)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now of these friends get their User objects.
|
||||||
|
for _, friend := range fs {
|
||||||
|
if requests {
|
||||||
|
userIDs = append(userIDs, friend.SourceUserID)
|
||||||
|
} else {
|
||||||
|
userIDs = append(userIDs, friend.TargetUserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetUsers(userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFriendRequests returns all pending friend requests for a user.
|
||||||
|
func GetFriendRequests(userID uint64) ([]*Friend, error) {
|
||||||
|
var fs = []*Friend{}
|
||||||
|
result := DB.Where(
|
||||||
|
"target_user_id = ? AND approved = ?",
|
||||||
|
userID,
|
||||||
|
false,
|
||||||
|
).Find(&fs)
|
||||||
|
return fs, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveFriend severs a friend connection both directions, used when
|
||||||
|
// rejecting a request or removing a friend.
|
||||||
|
func RemoveFriend(sourceUserID, targetUserID uint64) error {
|
||||||
|
result := DB.Where(
|
||||||
|
"(source_user_id = ? AND target_user_id = ?) OR "+
|
||||||
|
"(target_user_id = ? AND source_user_id = ?)",
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
sourceUserID, targetUserID,
|
||||||
|
).Delete(&Friend{})
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save photo.
|
||||||
|
func (f *Friend) Save() error {
|
||||||
|
result := DB.Save(f)
|
||||||
|
return result.Error
|
||||||
|
}
|
|
@ -13,4 +13,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&Photo{})
|
DB.AutoMigrate(&Photo{})
|
||||||
DB.AutoMigrate(&CertificationPhoto{})
|
DB.AutoMigrate(&CertificationPhoto{})
|
||||||
DB.AutoMigrate(&Message{})
|
DB.AutoMigrate(&Message{})
|
||||||
|
DB.AutoMigrate(&Friend{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,6 +79,24 @@ func GetUser(userId uint64) (*User, error) {
|
||||||
return user, result.Error
|
return user, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetUsers queries for multiple user IDs and returns users in the same order.
|
||||||
|
func GetUsers(userIDs []uint64) ([]*User, error) {
|
||||||
|
userMap, err := MapUsers(userIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-order them per the original sequence.
|
||||||
|
var users = []*User{}
|
||||||
|
for _, uid := range userIDs {
|
||||||
|
if user, ok := userMap[uid]; ok {
|
||||||
|
users = append(users, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FindUser by username or email.
|
// FindUser by username or email.
|
||||||
func FindUser(username string) (*User, error) {
|
func FindUser(username string) (*User, error) {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
|
@ -94,6 +112,78 @@ func FindUser(username string) (*User, error) {
|
||||||
return u, result.Error
|
return u, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserSearch config.
|
||||||
|
type UserSearch struct {
|
||||||
|
EmailOrUsername string
|
||||||
|
Gender string
|
||||||
|
Orientation string
|
||||||
|
MaritalStatus string
|
||||||
|
Certified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchUsers
|
||||||
|
func SearchUsers(search *UserSearch, pager *Pagination) ([]*User, error) {
|
||||||
|
if search == nil {
|
||||||
|
search = &UserSearch{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
users = []*User{}
|
||||||
|
query *gorm.DB
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if search.EmailOrUsername != "" {
|
||||||
|
ilike := "%" + strings.TrimSpace(strings.ToLower(search.EmailOrUsername)) + "%"
|
||||||
|
wheres = append(wheres, "(email LIKE ? OR username LIKE ?)")
|
||||||
|
placeholders = append(placeholders, ilike, ilike)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Gender != "" {
|
||||||
|
wheres = append(wheres, `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profile_fields
|
||||||
|
WHERE user_id = users.id AND name = ? AND value = ?
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
placeholders = append(placeholders, "gender", search.Gender)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Orientation != "" {
|
||||||
|
wheres = append(wheres, `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profile_fields
|
||||||
|
WHERE user_id = users.id AND name = ? AND value = ?
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
placeholders = append(placeholders, "orientation", search.Orientation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.MaritalStatus != "" {
|
||||||
|
wheres = append(wheres, `
|
||||||
|
EXISTS (
|
||||||
|
SELECT 1 FROM profile_fields
|
||||||
|
WHERE user_id = users.id AND name = ? AND value = ?
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
placeholders = append(placeholders, "marital_status", search.MaritalStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
if search.Certified {
|
||||||
|
wheres = append(wheres, "certified = ?")
|
||||||
|
placeholders = append(placeholders, search.Certified)
|
||||||
|
}
|
||||||
|
|
||||||
|
query = (&User{}).Preload().Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
)
|
||||||
|
query.Model(&User{}).Count(&pager.Total)
|
||||||
|
result := query.Offset(pager.GetOffset()).Limit(pager.PerPage).Find(&users)
|
||||||
|
return users, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
// UserMap helps map a set of users to look up by ID.
|
// UserMap helps map a set of users to look up by ID.
|
||||||
type UserMap map[uint64]*User
|
type UserMap map[uint64]*User
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/account"
|
"git.kirsle.net/apps/gosocial/pkg/controller/account"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/admin"
|
"git.kirsle.net/apps/gosocial/pkg/controller/admin"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/api"
|
"git.kirsle.net/apps/gosocial/pkg/controller/api"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/controller/friend"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/inbox"
|
"git.kirsle.net/apps/gosocial/pkg/controller/inbox"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/index"
|
"git.kirsle.net/apps/gosocial/pkg/controller/index"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
"git.kirsle.net/apps/gosocial/pkg/controller/photo"
|
||||||
|
@ -35,9 +36,12 @@ func New() http.Handler {
|
||||||
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
mux.Handle("/messages/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||||
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
mux.Handle("/messages/compose", middleware.LoginRequired(inbox.Compose()))
|
||||||
|
mux.Handle("/friends", middleware.LoginRequired(friend.Friends()))
|
||||||
|
mux.Handle("/friends/add", middleware.LoginRequired(friend.AddFriend()))
|
||||||
|
|
||||||
// Certification Required. Pages that only full (verified) members can access.
|
// Certification Required. Pages that only full (verified) members can access.
|
||||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||||
|
mux.Handle("/members", middleware.CertRequired(account.Search()))
|
||||||
|
|
||||||
// Admin endpoints.
|
// Admin endpoints.
|
||||||
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
||||||
|
|
|
@ -29,6 +29,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
m["LoggedIn"] = false
|
m["LoggedIn"] = false
|
||||||
m["CurrentUser"] = nil
|
m["CurrentUser"] = nil
|
||||||
m["NavUnreadMessages"] = 0
|
m["NavUnreadMessages"] = 0
|
||||||
|
m["NavFriendRequests"] = 0
|
||||||
|
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
|
@ -44,5 +45,12 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
} else {
|
} else {
|
||||||
log.Error("MergeUserVars: couldn't CountUnreadMessages for %d: %s", user.ID, err)
|
log.Error("MergeUserVars: couldn't CountUnreadMessages for %d: %s", user.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get friend request count.
|
||||||
|
if count, err := models.CountFriendRequests(user.ID); err == nil {
|
||||||
|
m["NavFriendRequests"] = count
|
||||||
|
} else {
|
||||||
|
log.Error("MergeUserVars: couldn't CountFriendRequests for %d: %s", user.ID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,16 +87,27 @@
|
||||||
<div class="level">
|
<div class="level">
|
||||||
<div class="level-item">
|
<div class="level-item">
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
|
<form action="/friends/add" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="username" value="{{.User.Username}}">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<button type="button" class="button">
|
<button type="submit" class="button"
|
||||||
|
{{if not (eq .IsFriend "none")}}title="Friendship {{.IsFriend}}"{{end}}>
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
{{if eq .IsFriend "approved"}}
|
||||||
|
<i class="fa fa-check has-text-success"></i>
|
||||||
|
{{else if eq .IsFriend "pending"}}
|
||||||
|
<i class="fa fa-spinner fa-spin"></i>
|
||||||
|
{{else}}
|
||||||
<i class="fa fa-plus"></i>
|
<i class="fa fa-plus"></i>
|
||||||
|
{{end}}
|
||||||
</span>
|
</span>
|
||||||
<span>Friend</span>
|
<span>Friend{{if eq .IsFriend "approved"}}s{{end}}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<a href="/messages/compose?to={{.User.Username}}" class="button">
|
<a href="/messages/compose?to={{.User.Username}}" class="button">
|
||||||
|
|
198
web/templates/account/search.html
Normal file
198
web/templates/account/search.html
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
{{define "title"}}Friends{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
{{$Root := .}}
|
||||||
|
<section class="hero is-link is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">People</h1>
|
||||||
|
<h2 class="subtitle">Explore</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<form action="/members" method="GET">
|
||||||
|
<div class="p-4">
|
||||||
|
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
Found {{.Pager.Total}} user{{Pluralize64 .Pager.Total}}
|
||||||
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<button type="submit"
|
||||||
|
class="button ml-6"
|
||||||
|
name="page"
|
||||||
|
value="{{.Pager.Previous}}"
|
||||||
|
{{if not .Pager.HasPrevious}}disabled{{end}}>Previous</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="button button-primary"
|
||||||
|
name="page"
|
||||||
|
value="{{.Pager.Next}}"
|
||||||
|
{{if not .Pager.HasNext}}disabled{{end}}>Next page</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<header class="card-header">
|
||||||
|
<p class="card-header-title">
|
||||||
|
Search Filters
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Certified:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="certified" name="certified">
|
||||||
|
<option value="true">Only certified users</option>
|
||||||
|
<option value="false"{{if eq $Root.Certified "false"}} selected{{end}}>Show all users</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label">Email or username:</label>
|
||||||
|
<input type="text" class="input"
|
||||||
|
name="username"
|
||||||
|
autocomplete="off"
|
||||||
|
value="{{$Root.EmailOrUsername}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="gender">Gender:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="gender" name="gender">
|
||||||
|
<option value=""></option>
|
||||||
|
{{range .Enum.Gender}}
|
||||||
|
<option value="{{.}}"{{if eq $Root.Gender .}} selected{{end}}>{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="orientation">Orientation:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="orientation" name="orientation">
|
||||||
|
<option value=""></option>
|
||||||
|
{{range .Enum.Orientation}}
|
||||||
|
<option value="{{.}}"{{if eq $Root.Orientation .}} selected{{end}}>{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="column">
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="marital_status">Relationship:</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
<select id="marital_status" name="marital_status">
|
||||||
|
<option value=""></option>
|
||||||
|
{{range .Enum.MaritalStatus}}
|
||||||
|
<option value="{{.}}"{{if eq $Root.MaritalStatus .}} selected{{end}}>{{.}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="has-text-centered">
|
||||||
|
<a href="/members" class="button">Reset</a>
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
<span>Search</span>
|
||||||
|
<span class="icon"><i class="fa fa-search"></i></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
|
{{range .Users}}
|
||||||
|
<div class="column is-half-tablet is-one-third-desktop">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media block">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">{{or .Name "(no name)"}}</a>
|
||||||
|
</p>
|
||||||
|
<p class="subtitle is-6 mb-2">
|
||||||
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||||
|
{{if not .Certified}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
|
<span>Not Certified!</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||||
|
<span>Admin</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
{{if .GetProfileField "city"}}
|
||||||
|
<p class="subtitle is-6 mb-2">
|
||||||
|
{{.GetProfileField "city"}}
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
<p class="subtitle is-7 mb-2">
|
||||||
|
{{if not .Birthdate.IsZero }}
|
||||||
|
<span class="mr-2">{{ComputeAge .Birthdate}}yo</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .GetProfileField "gender"}}
|
||||||
|
<span class="mr-2">{{.GetProfileField "gender"}}</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .GetProfileField "pronouns"}}
|
||||||
|
<span class="mr-2">{{.GetProfileField "pronouns"}}</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .GetProfileField "orientation"}}
|
||||||
|
<span class="mr-2">{{.GetProfileField "orientation"}}</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div><!-- media-block -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}<!-- range .Friends -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
|
@ -56,13 +56,13 @@
|
||||||
<a class="navbar-item" href="/friends">
|
<a class="navbar-item" href="/friends">
|
||||||
<span class="icon"><i class="fa fa-user-group"></i></span>
|
<span class="icon"><i class="fa fa-user-group"></i></span>
|
||||||
<span>Friends</span>
|
<span>Friends</span>
|
||||||
<!-- <span class="tag is-warning">42</span> -->
|
{{if .NavFriendRequests}}<span class="tag is-warning ml-1">{{.NavFriendRequests}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="navbar-item" href="/messages">
|
<a class="navbar-item" href="/messages">
|
||||||
<span class="icon"><i class="fa fa-envelope"></i></span>
|
<span class="icon"><i class="fa fa-envelope"></i></span>
|
||||||
<span>Messages</span>
|
<span>Messages</span>
|
||||||
{{if .NavUnreadMessages}}<span class="tag is-warning">{{.NavUnreadMessages}}</span>{{end}}
|
{{if .NavUnreadMessages}}<span class="tag is-warning ml-1">{{.NavUnreadMessages}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -72,6 +72,10 @@
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="navbar-dropdown">
|
<div class="navbar-dropdown">
|
||||||
|
<a class="navbar-item" href="/members">
|
||||||
|
<span class="icon"><i class="fa fa-people-group"></i></span>
|
||||||
|
<span>People</span>
|
||||||
|
</a>
|
||||||
<a class="navbar-item" href="/about">
|
<a class="navbar-item" href="/about">
|
||||||
About
|
About
|
||||||
</a>
|
</a>
|
||||||
|
|
133
web/templates/friend/friends.html
Normal file
133
web/templates/friend/friends.html
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
{{define "title"}}Friends{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
{{$Root := .}}
|
||||||
|
<section class="hero is-link is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">Friends</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="p-4 is-text-centered">
|
||||||
|
<div class="level">
|
||||||
|
<div class="level-item">
|
||||||
|
<div class="tabs is-toggle">
|
||||||
|
<ul>
|
||||||
|
<li{{if not .IsRequests}} class="is-active"{{end}}>
|
||||||
|
<a href="/friends">My Friends</a>
|
||||||
|
</li>
|
||||||
|
<li{{if .IsRequests}} class="is-active"{{end}}>
|
||||||
|
<a href="/friends?view=requests">
|
||||||
|
Requests
|
||||||
|
{{if .NavFriendRequests}}<span class="tag is-warning ml-2">{{.NavFriendRequests}}</span>{{end}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-4">
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
You have {{.Pager.Total}} friend{{if .IsRequests}} request{{end}}{{Pluralize64 .Pager.Total}}
|
||||||
|
(page {{.Pager.Page}} of {{.Pager.Pages}}).
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="block">
|
||||||
|
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||||
|
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
|
||||||
|
href="{{.Request.URL.Path}}?{{if .IsRequests}}view=requests&{{end}}page={{.Pager.Previous}}">Previous</a>
|
||||||
|
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
|
||||||
|
href="{{.Request.URL.Path}}?{{if .IsRequests}}view=requests&{{end}}page={{.Pager.Next}}">Next page</a>
|
||||||
|
<ul class="pagination-list">
|
||||||
|
{{$Root := .}}
|
||||||
|
{{range .Pager.Iter}}
|
||||||
|
<li>
|
||||||
|
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
|
||||||
|
aria-label="Page {{.Page}}"
|
||||||
|
href="{{$Root.Request.URL.Path}}?{{if $Root.IsRequests}}view=requests&{{end}}page={{.Page}}">
|
||||||
|
{{.Page}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
|
||||||
|
{{range .Friends}}
|
||||||
|
<div class="column is-half-tablet is-one-third-desktop">
|
||||||
|
|
||||||
|
<form action="/friends/add" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="username" value="{{.Username}}">
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-content">
|
||||||
|
<div class="media block">
|
||||||
|
<div class="media-left">
|
||||||
|
<figure class="image is-64x64">
|
||||||
|
{{if .ProfilePhoto.ID}}
|
||||||
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
|
{{else}}
|
||||||
|
<img src="/static/img/shy.png">
|
||||||
|
{{end}}
|
||||||
|
</figure>
|
||||||
|
</div>
|
||||||
|
<div class="media-content">
|
||||||
|
<p class="title is-4">{{or .Name "(no name)"}}</p>
|
||||||
|
<p class="subtitle is-6">
|
||||||
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||||
|
{{if not .Certified}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-certificate"></i></span>
|
||||||
|
<span>Not Certified!</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .IsAdmin}}
|
||||||
|
<span class="has-text-danger">
|
||||||
|
<span class="icon"><i class="fa fa-gavel"></i></span>
|
||||||
|
<span>Admin</span>
|
||||||
|
</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if $Root.IsRequests}}
|
||||||
|
<footer class="card-footer">
|
||||||
|
<button type="submit" name="verdict" value="approve" class="card-footer-item button is-success">
|
||||||
|
<span class="icon"><i class="fa fa-check"></i></span>
|
||||||
|
<span>Approve</span>
|
||||||
|
</button>
|
||||||
|
<button type="submit" name="verdict" value="reject" class="card-footer-item button is-danger">
|
||||||
|
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||||
|
<span>Reject</span>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
{{else}}
|
||||||
|
<footer class="card-footer">
|
||||||
|
<button type="submit" name="verdict" value="remove" class="card-footer-item button is-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to remove this friendship?')">
|
||||||
|
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||||
|
<span>Remove</span>
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}<!-- range .Friends -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
Reference in New Issue
Block a user