New search filters and friendship sent icon

This commit is contained in:
Noah Petherbridge 2023-09-01 17:12:27 -07:00
parent c8d09e6a17
commit 67a54c866e
7 changed files with 232 additions and 36 deletions

View File

@ -36,6 +36,7 @@ func Search() http.HandlerFunc {
orientation = r.FormValue("orientation")
maritalStatus = r.FormValue("marital_status")
hereFor = r.FormValue("here_for")
friendSearch = r.FormValue("friends") == "true"
sort = r.FormValue("sort")
sortOK bool
ageMin int
@ -93,6 +94,8 @@ func Search() http.HandlerFunc {
HereFor: hereFor,
Certified: isCertified != "false",
InnerCircle: isCertified == "circle",
ShyAccounts: isCertified == "shy",
Friends: friendSearch,
AgeMin: ageMin,
AgeMax: ageMax,
}, pager)
@ -117,11 +120,18 @@ func Search() http.HandlerFunc {
"EmailOrUsername": username,
"AgeMin": ageMin,
"AgeMax": ageMax,
"FriendSearch": friendSearch,
"Sort": sort,
// Photo counts mapped to users
"PhotoCountMap": models.MapPhotoCounts(users),
// Map Shy Account badges for these results
"ShyMap": models.MapShyAccounts(users),
// Map friendships to these users.
"FriendMap": models.MapFriends(currentUser, users),
// Current user's location setting.
"MyLocation": myLocation,
"GeoIPInsights": insights,

View File

@ -297,3 +297,45 @@ func (f *Friend) Save() error {
result := DB.Save(f)
return result.Error
}
// FriendMap maps user IDs to friendship status for the current user.
type FriendMap map[uint64]bool
// MapFriends looks up a set of user IDs in bulk and returns a FriendMap suitable for templates.
func MapFriends(currentUser *User, users []*User) FriendMap {
var (
usermap = FriendMap{}
set = map[uint64]interface{}{}
distinct = []uint64{}
)
// Uniqueify users.
for _, user := range users {
if _, ok := set[user.ID]; ok {
continue
}
set[user.ID] = nil
distinct = append(distinct, user.ID)
}
var (
matched = []*Friend{}
result = DB.Model(&Friend{}).Where(
"source_user_id = ? AND target_user_id IN ? AND approved = ?",
currentUser.ID, distinct, true,
).Find(&matched)
)
if result.Error == nil {
for _, row := range matched {
usermap[row.TargetUserID] = true
}
}
return usermap
}
// Get a user from the FriendMap.
func (um FriendMap) Get(id uint64) bool {
return um[id]
}

View File

@ -0,0 +1,98 @@
package models
// Supplementary functions to do with Shy Accounts.
import "code.nonshy.com/nonshy/website/pkg/log"
// IsShy returns whether the user might have an "empty" profile from the perspective of anybody.
//
// An empty profile means their profile is Private or else ALL of their photos are non-public; so that
// somebody viewing their page might see nothing at all from them and consider them a "blank" profile.
func (u *User) IsShy() bool {
// NOTE: if you change the logic for Shy Accounts, also align your changes
// in the functions: WhereClauseShyAccounts.
// Non-certified users are considered empty.
if !u.Certified {
return true
}
// Private profile automatically applies.
if u.Visibility == UserVisibilityPrivate {
return true
}
// If ALL of our photos are non-public, that counts too.
var photoTypes = u.DistinctPhotoTypes()
if _, ok := photoTypes[PhotoPublic]; !ok {
log.Info("IsEmptyProfile: true because visibilities %+v did not include public", photoTypes)
return true
}
return false
}
// WhereClauseShyAccounts returns SQL query fragments when running User table queries
// that will filter for shy accounts.
//
// This is used by SearchUsers and MapShyAccounts.
func WhereClauseShyAccounts() (where string, placeholders []interface{}) {
where = `(
certified IS NOT true
OR visibility = ?
OR NOT EXISTS (
SELECT 1 FROM photos
WHERE user_id = users.id
AND visibility = ?
)
)`
placeholders = []interface{}{
UserVisibilityPrivate,
PhotoPublic,
}
return
}
// ShyMap maps user IDs to Shy Account status in bulk queries.
type ShyMap map[uint64]bool
// MapShyAccounts looks up a set of user IDs in bulk and returns a ShyMap suitable for templates.
func MapShyAccounts(users []*User) ShyMap {
var (
usermap = ShyMap{}
set = map[uint64]interface{}{}
distinct = []uint64{}
)
// Uniqueify users.
for _, user := range users {
if _, ok := set[user.ID]; ok {
continue
}
set[user.ID] = nil
distinct = append(distinct, user.ID)
}
var (
matched = []*User{}
where, placeholders = WhereClauseShyAccounts()
result = (&User{}).
Preload().
Where("id IN ?", distinct).
Where(where, placeholders...).
Find(&matched)
)
if result.Error == nil {
for _, row := range matched {
usermap[row.ID] = true
}
}
return usermap
}
// Get a user from the ShyMap.
func (um ShyMap) Get(id uint64) bool {
return um[id]
}

View File

@ -139,31 +139,6 @@ func FindUser(username string) (*User, error) {
return u, result.Error
}
// IsShy returns whether the user might have an "empty" profile from the perspective of anybody.
//
// An empty profile means their profile is Private or else ALL of their photos are non-public; so that
// somebody viewing their page might see nothing at all from them and consider them a "blank" profile.
func (u *User) IsShy() bool {
// Non-certified users are considered empty.
if !u.Certified {
return true
}
// Private profile automatically applies.
if u.Visibility == UserVisibilityPrivate {
return true
}
// If ALL of our photos are non-public, that counts too.
var photoTypes = u.DistinctPhotoTypes()
if _, ok := photoTypes[PhotoPublic]; !ok {
log.Info("IsEmptyProfile: true because visibilities %+v did not include public", photoTypes)
return true
}
return false
}
// IsShyFrom tells whether the user is shy from the perspective of the other user.
//
// That is, depending on our profile visibility and friendship status.
@ -191,6 +166,8 @@ type UserSearch struct {
HereFor string
Certified bool
InnerCircle bool
ShyAccounts bool
Friends bool
AgeMin int
AgeMax int
}
@ -289,6 +266,7 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er
placeholders = append(placeholders, "here_for", "%"+search.HereFor+"%")
}
// Certified filter (including if Shy Accounts are asked for)
if search.Certified {
wheres = append(wheres, "certified = ?", "status = ?")
placeholders = append(placeholders, search.Certified, UserStatusActive)
@ -299,6 +277,27 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er
placeholders = append(placeholders, true, true)
}
if search.ShyAccounts {
a, b := WhereClauseShyAccounts()
wheres = append(wheres, a)
placeholders = append(placeholders, b...)
}
if search.Friends {
wheres = append(wheres, `
EXISTS (
SELECT 1 FROM friends
WHERE source_user_id = ?
AND target_user_id = users.id
AND approved = ?
)
`)
placeholders = append(placeholders,
user.ID,
true,
)
}
if search.AgeMin > 0 {
date := time.Now().AddDate(-search.AgeMin, 0, 0)
wheres = append(wheres, "birthdate <= ?")

View File

@ -164,7 +164,7 @@
{{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>
<i class="fa fa-paper-plane"></i>
{{else}}
<i class="fa fa-plus"></i>
{{end}}

View File

@ -29,7 +29,11 @@
</div>
</div>
{{if not (eq .Sort "distance")}}
{{if .FriendSearch}}
<div class="notification is-success is-light">
Currently searching within your <i class="fa fa-user-group"></i> Friends list.
</div>
{{else if not (eq .Sort "distance")}}
<div class="notification is-success is-light">
<strong>New feature:</strong> you can now see <strong>Who's Nearby!</strong>
{{if not .MyLocation.Source}}
@ -72,22 +76,24 @@
<div class="card-content">
<div class="columns">
<div class="column">
<div class="column pr-1">
<div class="field">
<label class="label">Status:</label>
<div class="select is-fullwidth">
<select id="certified" name="certified">
<option value="true">Only certified users</option>
<option value="friends"{{if eq $Root.Certified "friends"}} selected{{end}}>Friends only</option>
{{if .CurrentUser.IsInnerCircle}}
<option value="circle"{{if eq $Root.Certified "circle"}} selected{{end}}>Inner circle only</option>
{{end}}
<option value="shy"{{if eq $Root.Certified "shy"}} selected{{end}}>Shy Accounts</option>
<option value="false"{{if eq $Root.Certified "false"}} selected{{end}}>Show all users</option>
</select>
</div>
</div>
</div>
<div class="column">
<div class="column px-1">
<div class="field">
<label class="label">Partial username:</label>
<input type="text" class="input"
@ -97,7 +103,7 @@
</div>
</div>
<div class="column">
<div class="column px-1">
<div class="field">
<label class="label">Age:</label>
<div class="columns is-mobile is-gapless">
@ -125,7 +131,7 @@
</div>
</div>
<div class="column">
<div class="column pl-1">
<div class="field">
<label class="label" for="gender">Gender:</label>
<div class="select is-fullwidth">
@ -142,7 +148,7 @@
</div>
<div class="columns is-centered">
<div class="column">
<div class="column pr-1">
<div class="field">
<label class="label" for="orientation">Orientation:</label>
<div class="select is-fullwidth">
@ -156,7 +162,7 @@
</div>
</div>
<div class="column">
<div class="column px-1">
<div class="field">
<label class="label" for="marital_status">Relationship:</label>
<div class="select is-fullwidth">
@ -170,7 +176,7 @@
</div>
</div>
<div class="column">
<div class="column px-1">
<div class="field">
<label class="label" for="here_for">Here for:</label>
<div class="select is-fullwidth">
@ -184,7 +190,21 @@
</div>
</div>
<div class="column">
<div class="column px-1">
<div class="field">
<label class="label" for="friends">Friendship:</label>
<label class="checkbox">
<input type="checkbox"
name="friends"
id="friends"
value="true"
{{if .FriendSearch}}checked{{end}}>
Show only my friends
</label>
</div>
</div>
<div class="column px-1">
<div class="field">
<label class="label" for="sort">Sort by:</label>
<div class="select is-full-width">
@ -201,7 +221,7 @@
</div>
</div>
<div class="column has-text-right">
<div class="column pl-1 has-text-right">
<a href="/members" class="button">Reset</a>
<button type="submit" class="button is-success">
<span>Search</span>
@ -226,6 +246,16 @@
<div class="media block">
<div class="media-left">
{{template "avatar-64x64" .}}
<!-- Friendship badge -->
{{if $Root.FriendMap.Get .ID}}
<div class="has-text-centered">
<span class="is-size-7 has-text-warning-dark">
<i class="fa fa-user-group" title="Friends"></i>
Friends
</span>
</div>
{{end}}
</div>
<div class="media-content">
<p class="title is-4">
@ -246,11 +276,18 @@
<p class="subtitle is-6 mb-2">
<span class="icon"><i class="fa fa-user"></i></span>
<a href="/u/{{.Username}}">{{.Username}}</a>
<!-- Not Certified or Shy Account badge -->
{{if not .Certified}}
<span class="has-text-danger is-size-7">
<i class="fa fa-certificate"></i>
<span>Not Certified!</span>
</span>
{{else if $Root.ShyMap.Get .ID}}
<span class="has-text-danger is-size-7">
<i class="fa fa-ghost"></i>
<span>Shy Account</span>
</span>
{{end}}
<!-- "(banned)" label -->

View File

@ -40,6 +40,12 @@
<div class="p-4">
<div class="notification is-success is-light">
<strong>New feature:</strong>
you can now <a href="/members?friends=true">search and sort</a> your friends list
in the <i class="fa fa-people-group"></i> Member Directory!
</div>
<div class="block">
{{if .IsPending}}
You have sent {{.Pager.Total}} friend request{{Pluralize64 .Pager.Total}} which
@ -129,6 +135,10 @@
{{end}}<!-- range .Friends -->
</div>
<div class="block">
{{SimplePager .Pager}}
</div>
</div>
</div>
{{end}}