Friend Requests and User Search
This commit is contained in:
parent
788442d7e9
commit
4adffe9fa9
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"
|
||||
|
||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||
)
|
||||
|
||||
|
@ -21,6 +22,14 @@ func Profile() http.HandlerFunc {
|
|||
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.
|
||||
user, err := models.FindUser(username)
|
||||
if err != nil {
|
||||
|
@ -30,6 +39,7 @@ func Profile() http.HandlerFunc {
|
|||
|
||||
vars := map[string]interface{}{
|
||||
"User": user,
|
||||
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
|
||||
}
|
||||
|
||||
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"
|
||||
"strconv"
|
||||
|
||||
"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"
|
||||
|
@ -58,7 +59,7 @@ func Inbox() http.HandlerFunc {
|
|||
|
||||
// Get the full chat thread (paginated).
|
||||
threadPager = &models.Pagination{
|
||||
PerPage: 5,
|
||||
PerPage: config.PageSizeInboxThread,
|
||||
Sort: "created_at desc",
|
||||
}
|
||||
threadPager.ParsePage(r)
|
||||
|
@ -84,7 +85,7 @@ func Inbox() http.HandlerFunc {
|
|||
// Get the inbox list of messages.
|
||||
pager := &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: 5,
|
||||
PerPage: config.PageSizeInboxList,
|
||||
Sort: "created_at desc",
|
||||
}
|
||||
if viewThread == nil {
|
||||
|
|
|
@ -271,7 +271,7 @@ func AdminCertification() http.HandlerFunc {
|
|||
// Get the pending photos.
|
||||
pager := &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: 20,
|
||||
PerPage: config.PageSizeAdminCertification,
|
||||
Sort: "updated_at desc",
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
|
|
|
@ -3,6 +3,7 @@ package photo
|
|||
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"
|
||||
|
@ -29,7 +30,7 @@ func SiteGallery() http.HandlerFunc {
|
|||
// Get the page of photos.
|
||||
pager := &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: 8,
|
||||
PerPage: config.PageSizeSiteGallery,
|
||||
Sort: "created_at desc",
|
||||
}
|
||||
pager.ParsePage(r)
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||
|
@ -49,6 +50,8 @@ func UserPhotos() http.HandlerFunc {
|
|||
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
||||
if isOwnPhotos || currentUser.IsAdmin {
|
||||
visibility = append(visibility, models.PhotoFriends, models.PhotoPrivate)
|
||||
} else if models.AreFriends(user.ID, currentUser.ID) {
|
||||
visibility = append(visibility, models.PhotoFriends)
|
||||
}
|
||||
|
||||
// Explicit photo filter?
|
||||
|
@ -60,7 +63,7 @@ func UserPhotos() http.HandlerFunc {
|
|||
// Get the page of photos.
|
||||
pager := &models.Pagination{
|
||||
Page: 1,
|
||||
PerPage: 8,
|
||||
PerPage: config.PageSizeUserGallery,
|
||||
Sort: "created_at desc",
|
||||
}
|
||||
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(&CertificationPhoto{})
|
||||
DB.AutoMigrate(&Message{})
|
||||
DB.AutoMigrate(&Friend{})
|
||||
}
|
||||
|
|
|
@ -79,6 +79,24 @@ func GetUser(userId uint64) (*User, 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.
|
||||
func FindUser(username string) (*User, error) {
|
||||
if username == "" {
|
||||
|
@ -94,6 +112,78 @@ func FindUser(username string) (*User, 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.
|
||||
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/admin"
|
||||
"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/index"
|
||||
"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/read/", middleware.LoginRequired(inbox.Inbox()))
|
||||
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.
|
||||
mux.Handle("/photo/gallery", middleware.CertRequired(photo.SiteGallery()))
|
||||
mux.Handle("/members", middleware.CertRequired(account.Search()))
|
||||
|
||||
// Admin endpoints.
|
||||
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
||||
|
|
|
@ -29,6 +29,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
m["LoggedIn"] = false
|
||||
m["CurrentUser"] = nil
|
||||
m["NavUnreadMessages"] = 0
|
||||
m["NavFriendRequests"] = 0
|
||||
|
||||
if r == nil {
|
||||
return
|
||||
|
@ -44,5 +45,12 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
|||
} else {
|
||||
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-item">
|
||||
<div class="field has-addons">
|
||||
<form action="/friends/add" method="POST">
|
||||
{{InputCSRF}}
|
||||
<input type="hidden" name="username" value="{{.User.Username}}">
|
||||
<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">
|
||||
{{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>
|
||||
{{end}}
|
||||
</span>
|
||||
<span>Friend</span>
|
||||
<span>Friend{{if eq .IsFriend "approved"}}s{{end}}</span>
|
||||
</span>
|
||||
</button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<p class="control">
|
||||
<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">
|
||||
<span class="icon"><i class="fa fa-user-group"></i></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 class="navbar-item" href="/messages">
|
||||
<span class="icon"><i class="fa fa-envelope"></i></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>
|
||||
{{end}}
|
||||
|
||||
|
@ -72,6 +72,10 @@
|
|||
</a>
|
||||
|
||||
<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">
|
||||
About
|
||||
</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}}
|
Loading…
Reference in New Issue
Block a user