New search filters and friendship sent icon

face-detect
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") orientation = r.FormValue("orientation")
maritalStatus = r.FormValue("marital_status") maritalStatus = r.FormValue("marital_status")
hereFor = r.FormValue("here_for") hereFor = r.FormValue("here_for")
friendSearch = r.FormValue("friends") == "true"
sort = r.FormValue("sort") sort = r.FormValue("sort")
sortOK bool sortOK bool
ageMin int ageMin int
@ -93,6 +94,8 @@ func Search() http.HandlerFunc {
HereFor: hereFor, HereFor: hereFor,
Certified: isCertified != "false", Certified: isCertified != "false",
InnerCircle: isCertified == "circle", InnerCircle: isCertified == "circle",
ShyAccounts: isCertified == "shy",
Friends: friendSearch,
AgeMin: ageMin, AgeMin: ageMin,
AgeMax: ageMax, AgeMax: ageMax,
}, pager) }, pager)
@ -117,11 +120,18 @@ func Search() http.HandlerFunc {
"EmailOrUsername": username, "EmailOrUsername": username,
"AgeMin": ageMin, "AgeMin": ageMin,
"AgeMax": ageMax, "AgeMax": ageMax,
"FriendSearch": friendSearch,
"Sort": sort, "Sort": sort,
// Photo counts mapped to users // Photo counts mapped to users
"PhotoCountMap": models.MapPhotoCounts(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. // Current user's location setting.
"MyLocation": myLocation, "MyLocation": myLocation,
"GeoIPInsights": insights, "GeoIPInsights": insights,

View File

@ -297,3 +297,45 @@ func (f *Friend) Save() error {
result := DB.Save(f) result := DB.Save(f)
return result.Error 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 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. // IsShyFrom tells whether the user is shy from the perspective of the other user.
// //
// That is, depending on our profile visibility and friendship status. // That is, depending on our profile visibility and friendship status.
@ -191,6 +166,8 @@ type UserSearch struct {
HereFor string HereFor string
Certified bool Certified bool
InnerCircle bool InnerCircle bool
ShyAccounts bool
Friends bool
AgeMin int AgeMin int
AgeMax int AgeMax int
} }
@ -289,6 +266,7 @@ func SearchUsers(user *User, search *UserSearch, pager *Pagination) ([]*User, er
placeholders = append(placeholders, "here_for", "%"+search.HereFor+"%") placeholders = append(placeholders, "here_for", "%"+search.HereFor+"%")
} }
// Certified filter (including if Shy Accounts are asked for)
if search.Certified { if search.Certified {
wheres = append(wheres, "certified = ?", "status = ?") wheres = append(wheres, "certified = ?", "status = ?")
placeholders = append(placeholders, search.Certified, UserStatusActive) 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) 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 { if search.AgeMin > 0 {
date := time.Now().AddDate(-search.AgeMin, 0, 0) date := time.Now().AddDate(-search.AgeMin, 0, 0)
wheres = append(wheres, "birthdate <= ?") wheres = append(wheres, "birthdate <= ?")

View File

@ -164,7 +164,7 @@
{{if eq .IsFriend "approved"}} {{if eq .IsFriend "approved"}}
<i class="fa fa-check has-text-success"></i> <i class="fa fa-check has-text-success"></i>
{{else if eq .IsFriend "pending"}} {{else if eq .IsFriend "pending"}}
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-paper-plane"></i>
{{else}} {{else}}
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
{{end}} {{end}}

View File

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

View File

@ -40,6 +40,12 @@
<div class="p-4"> <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"> <div class="block">
{{if .IsPending}} {{if .IsPending}}
You have sent {{.Pager.Total}} friend request{{Pluralize64 .Pager.Total}} which You have sent {{.Pager.Total}} friend request{{Pluralize64 .Pager.Total}} which
@ -129,6 +135,10 @@
{{end}}<!-- range .Friends --> {{end}}<!-- range .Friends -->
</div> </div>
<div class="block">
{{SimplePager .Pager}}
</div>
</div> </div>
</div> </div>
{{end}} {{end}}