appview: implement GetFollowersFollowingCounts to query follow stats for multiple dids in one go #505

merged
opened by ptr.pet targeting master from ptr.pet/core: followers-following-list
  • moves FollowStats type from timeline.go into follow.go
  • makes existing GetFollowersFollowingCount also use FollowStats

Signed-off-by: dusk y.bera003.06@protonmail.com

Changed files
+110 -32
appview
+79 -3
appview/db/follow.go
···
return err
}
-
func GetFollowerFollowingCount(e Execer, did string) (int, int, error) {
+
type FollowStats struct {
+
Followers int
+
Following int
+
}
+
+
func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) {
followers, following := 0, 0
err := e.QueryRow(
`SELECT
···
COUNT(CASE WHEN user_did = ? THEN 1 END) AS following
FROM follows;`, did, did).Scan(&followers, &following)
if err != nil {
-
return 0, 0, err
+
return FollowStats{}, err
+
}
+
return FollowStats{
+
Followers: followers,
+
Following: following,
+
}, nil
+
}
+
+
func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]FollowStats, error) {
+
if len(dids) == 0 {
+
return nil, nil
+
}
+
+
placeholders := make([]string, len(dids))
+
for i := range placeholders {
+
placeholders[i] = "?"
}
-
return followers, following, nil
+
placeholderStr := strings.Join(placeholders, ",")
+
+
args := make([]any, len(dids)*2)
+
for i, did := range dids {
+
args[i] = did
+
args[i+len(dids)] = did
+
}
+
+
query := fmt.Sprintf(`
+
select
+
coalesce(f.did, g.did) as did,
+
coalesce(f.followers, 0) as followers,
+
coalesce(g.following, 0) as following
+
from (
+
select subject_did as did, count(*) as followers
+
from follows
+
where subject_did in (%s)
+
group by subject_did
+
) f
+
full outer join (
+
select user_did as did, count(*) as following
+
from follows
+
where user_did in (%s)
+
group by user_did
+
) g on f.did = g.did`,
+
placeholderStr, placeholderStr)
+
+
result := make(map[string]FollowStats)
+
+
rows, err := e.Query(query, args...)
+
if err != nil {
+
return nil, err
+
}
+
defer rows.Close()
+
+
for rows.Next() {
+
var did string
+
var followers, following int
+
if err := rows.Scan(&did, &followers, &following); err != nil {
+
return nil, err
+
}
+
result[did] = FollowStats{
+
Followers: followers,
+
Following: following,
+
}
+
}
+
+
for _, did := range dids {
+
if _, exists := result[did]; !exists {
+
result[did] = FollowStats{
+
Followers: 0,
+
Following: 0,
+
}
+
}
+
}
+
+
return result, nil
}
func GetFollows(e Execer, limit int, filters ...filter) ([]Follow, error) {
+3 -15
appview/db/timeline.go
···
*FollowStats
}
-
type FollowStats struct {
-
Followers int
-
Following int
-
}
-
const Limit = 50
// TODO: this gathers heterogenous events from different sources and aggregates
···
return nil, err
}
-
followStatMap := make(map[string]FollowStats)
-
for _, s := range subjects {
-
followers, following, err := GetFollowerFollowingCount(e, s)
-
if err != nil {
-
return nil, err
-
}
-
followStatMap[s] = FollowStats{
-
Followers: followers,
-
Following: following,
-
}
+
followStatMap, err := GetFollowerFollowingCounts(e, subjects)
+
if err != nil {
+
return nil, err
}
var events []TimelineEvent
+25 -11
appview/state/profile.go
···
log.Printf("getting profile data for %s: %s", did, err)
}
-
followersCount, followingCount, err := db.GetFollowerFollowingCount(s.db, did)
+
followStats, err := db.GetFollowerFollowingCount(s.db, did)
if err != nil {
log.Printf("getting follow stats for %s: %s", did, err)
}
···
UserHandle: ident.Handle.String(),
Profile: profile,
FollowStatus: followStatus,
-
FollowersCount: followersCount,
-
FollowingCount: followingCount,
+
FollowersCount: followStats.Followers,
+
FollowingCount: followStats.Following,
},
}
}
···
}
id := pageWithProfile.Id
+
loggedInUser := pageWithProfile.LoggedInUser
follows, err := fetchFollows(s.db, id.DID.String())
if err != nil {
···
}
if len(follows) == 0 {
-
return nil
+
return &FollowsPageParams{
+
LoggedInUser: loggedInUser,
+
Follows: []pages.FollowCard{},
+
Card: pageWithProfile.Card,
+
}
}
followDids := make([]string, 0, len(follows))
···
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
if err != nil {
log.Printf("getting profile for %s: %s", followDids, err)
-
return nil
}
-
loggedInUser := pageWithProfile.LoggedInUser
+
followStatsMap, err := db.GetFollowerFollowingCounts(s.db, followDids)
+
if err != nil {
+
log.Printf("getting follow counts for %s: %s", followDids, err)
+
}
+
var loggedInUserFollowing map[string]struct{}
if loggedInUser != nil {
following, err := db.GetFollowing(s.db, loggedInUser.Did)
···
followCards := make([]pages.FollowCard, 0, len(follows))
for _, did := range followDids {
-
followersCount, followingCount, err := db.GetFollowerFollowingCount(s.db, did)
-
if err != nil {
-
log.Printf("getting follow stats for %s: %s", did, err)
+
followStats, exists := followStatsMap[did]
+
if !exists {
+
followStats = db.FollowStats{}
}
followStatus := db.IsNotFollowing
if loggedInUserFollowing != nil {
···
followCards = append(followCards, pages.FollowCard{
UserDid: did,
FollowStatus: followStatus,
-
FollowersCount: followersCount,
-
FollowingCount: followingCount,
+
FollowersCount: followStats.Followers,
+
FollowingCount: followStats.Following,
Profile: profile,
})
}
···
func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
followPage := s.followPage(w, r, db.GetFollowers, func(f db.Follow) string { return f.UserDid })
+
if followPage == nil {
+
return
+
}
s.pages.FollowersPage(w, pages.FollowersPageParams{
LoggedInUser: followPage.LoggedInUser,
···
func (s *State) followingPage(w http.ResponseWriter, r *http.Request) {
followPage := s.followPage(w, r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid })
+
if followPage == nil {
+
return
+
}
s.pages.FollowingPage(w, pages.FollowingPageParams{
LoggedInUser: followPage.LoggedInUser,
+3 -3
appview/strings/strings.go
···
followStatus = db.GetFollowStatus(s.Db, loggedInUser.Did, id.DID.String())
}
-
followersCount, followingCount, err := db.GetFollowerFollowingCount(s.Db, id.DID.String())
+
followStats, err := db.GetFollowerFollowingCount(s.Db, id.DID.String())
if err != nil {
l.Error("failed to get follow stats", "err", err)
}
···
UserHandle: id.Handle.String(),
Profile: profile,
FollowStatus: followStatus,
-
FollowersCount: followersCount,
-
FollowingCount: followingCount,
+
FollowersCount: followStats.Followers,
+
FollowingCount: followStats.Following,
},
Strings: all,
})