+26
-57
appview/db/follow.go
+26
-57
appview/db/follow.go
···
5
"log"
6
"strings"
7
"time"
8
)
9
10
-
type Follow struct {
11
-
UserDid string
12
-
SubjectDid string
13
-
FollowedAt time.Time
14
-
Rkey string
15
-
}
16
-
17
-
func AddFollow(e Execer, follow *Follow) error {
18
query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)`
19
_, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey)
20
return err
21
}
22
23
// Get a follow record
24
-
func GetFollow(e Execer, userDid, subjectDid string) (*Follow, error) {
25
query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?`
26
row := e.QueryRow(query, userDid, subjectDid)
27
28
-
var follow Follow
29
var followedAt string
30
err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey)
31
if err != nil {
···
55
return err
56
}
57
58
-
type FollowStats struct {
59
-
Followers int64
60
-
Following int64
61
-
}
62
-
63
-
func GetFollowerFollowingCount(e Execer, did string) (FollowStats, error) {
64
var followers, following int64
65
err := e.QueryRow(
66
`SELECT
···
68
COUNT(CASE WHEN user_did = ? THEN 1 END) AS following
69
FROM follows;`, did, did).Scan(&followers, &following)
70
if err != nil {
71
-
return FollowStats{}, err
72
}
73
-
return FollowStats{
74
Followers: followers,
75
Following: following,
76
}, nil
77
}
78
79
-
func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]FollowStats, error) {
80
if len(dids) == 0 {
81
return nil, nil
82
}
···
112
) g on f.did = g.did`,
113
placeholderStr, placeholderStr)
114
115
-
result := make(map[string]FollowStats)
116
117
rows, err := e.Query(query, args...)
118
if err != nil {
···
126
if err := rows.Scan(&did, &followers, &following); err != nil {
127
return nil, err
128
}
129
-
result[did] = FollowStats{
130
Followers: followers,
131
Following: following,
132
}
···
134
135
for _, did := range dids {
136
if _, exists := result[did]; !exists {
137
-
result[did] = FollowStats{
138
Followers: 0,
139
Following: 0,
140
}
···
144
return result, nil
145
}
146
147
-
func GetFollows(e Execer, limit int, filters ...filter) ([]Follow, error) {
148
-
var follows []Follow
149
150
var conditions []string
151
var args []any
···
177
return nil, err
178
}
179
for rows.Next() {
180
-
var follow Follow
181
var followedAt string
182
err := rows.Scan(
183
&follow.UserDid,
···
200
return follows, nil
201
}
202
203
-
func GetFollowers(e Execer, did string) ([]Follow, error) {
204
return GetFollows(e, 0, FilterEq("subject_did", did))
205
}
206
207
-
func GetFollowing(e Execer, did string) ([]Follow, error) {
208
return GetFollows(e, 0, FilterEq("user_did", did))
209
}
210
211
-
type FollowStatus int
212
-
213
-
const (
214
-
IsNotFollowing FollowStatus = iota
215
-
IsFollowing
216
-
IsSelf
217
-
)
218
-
219
-
func (s FollowStatus) String() string {
220
-
switch s {
221
-
case IsNotFollowing:
222
-
return "IsNotFollowing"
223
-
case IsFollowing:
224
-
return "IsFollowing"
225
-
case IsSelf:
226
-
return "IsSelf"
227
-
default:
228
-
return "IsNotFollowing"
229
-
}
230
-
}
231
-
232
-
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) {
233
if len(subjectDids) == 0 || userDid == "" {
234
-
return make(map[string]FollowStatus), nil
235
}
236
237
-
result := make(map[string]FollowStatus)
238
239
for _, subjectDid := range subjectDids {
240
if userDid == subjectDid {
241
-
result[subjectDid] = IsSelf
242
} else {
243
-
result[subjectDid] = IsNotFollowing
244
}
245
}
246
···
281
if err := rows.Scan(&subjectDid); err != nil {
282
return nil, err
283
}
284
-
result[subjectDid] = IsFollowing
285
}
286
287
return result, nil
288
}
289
290
-
func GetFollowStatus(e Execer, userDid, subjectDid string) FollowStatus {
291
statuses, err := getFollowStatuses(e, userDid, []string{subjectDid})
292
if err != nil {
293
-
return IsNotFollowing
294
}
295
return statuses[subjectDid]
296
}
297
298
-
func GetFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]FollowStatus, error) {
299
return getFollowStatuses(e, userDid, subjectDids)
300
}
···
5
"log"
6
"strings"
7
"time"
8
+
9
+
"tangled.org/core/appview/models"
10
)
11
12
+
func AddFollow(e Execer, follow *models.Follow) error {
13
query := `insert or ignore into follows (user_did, subject_did, rkey) values (?, ?, ?)`
14
_, err := e.Exec(query, follow.UserDid, follow.SubjectDid, follow.Rkey)
15
return err
16
}
17
18
// Get a follow record
19
+
func GetFollow(e Execer, userDid, subjectDid string) (*models.Follow, error) {
20
query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?`
21
row := e.QueryRow(query, userDid, subjectDid)
22
23
+
var follow models.Follow
24
var followedAt string
25
err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey)
26
if err != nil {
···
50
return err
51
}
52
53
+
func GetFollowerFollowingCount(e Execer, did string) (models.FollowStats, error) {
54
var followers, following int64
55
err := e.QueryRow(
56
`SELECT
···
58
COUNT(CASE WHEN user_did = ? THEN 1 END) AS following
59
FROM follows;`, did, did).Scan(&followers, &following)
60
if err != nil {
61
+
return models.FollowStats{}, err
62
}
63
+
return models.FollowStats{
64
Followers: followers,
65
Following: following,
66
}, nil
67
}
68
69
+
func GetFollowerFollowingCounts(e Execer, dids []string) (map[string]models.FollowStats, error) {
70
if len(dids) == 0 {
71
return nil, nil
72
}
···
102
) g on f.did = g.did`,
103
placeholderStr, placeholderStr)
104
105
+
result := make(map[string]models.FollowStats)
106
107
rows, err := e.Query(query, args...)
108
if err != nil {
···
116
if err := rows.Scan(&did, &followers, &following); err != nil {
117
return nil, err
118
}
119
+
result[did] = models.FollowStats{
120
Followers: followers,
121
Following: following,
122
}
···
124
125
for _, did := range dids {
126
if _, exists := result[did]; !exists {
127
+
result[did] = models.FollowStats{
128
Followers: 0,
129
Following: 0,
130
}
···
134
return result, nil
135
}
136
137
+
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138
+
var follows []models.Follow
139
140
var conditions []string
141
var args []any
···
167
return nil, err
168
}
169
for rows.Next() {
170
+
var follow models.Follow
171
var followedAt string
172
err := rows.Scan(
173
&follow.UserDid,
···
190
return follows, nil
191
}
192
193
+
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194
return GetFollows(e, 0, FilterEq("subject_did", did))
195
}
196
197
+
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198
return GetFollows(e, 0, FilterEq("user_did", did))
199
}
200
201
+
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
202
if len(subjectDids) == 0 || userDid == "" {
203
+
return make(map[string]models.FollowStatus), nil
204
}
205
206
+
result := make(map[string]models.FollowStatus)
207
208
for _, subjectDid := range subjectDids {
209
if userDid == subjectDid {
210
+
result[subjectDid] = models.IsSelf
211
} else {
212
+
result[subjectDid] = models.IsNotFollowing
213
}
214
}
215
···
250
if err := rows.Scan(&subjectDid); err != nil {
251
return nil, err
252
}
253
+
result[subjectDid] = models.IsFollowing
254
}
255
256
return result, nil
257
}
258
259
+
func GetFollowStatus(e Execer, userDid, subjectDid string) models.FollowStatus {
260
statuses, err := getFollowStatuses(e, userDid, []string{subjectDid})
261
if err != nil {
262
+
return models.IsNotFollowing
263
}
264
return statuses[subjectDid]
265
}
266
267
+
func GetFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
268
return getFollowStatuses(e, userDid, subjectDids)
269
}
+6
-5
appview/db/timeline.go
+6
-5
appview/db/timeline.go
···
5
"time"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
)
9
10
type TimelineEvent struct {
11
*Repo
12
-
*Follow
13
*Star
14
15
EventAt time.Time
···
19
20
// optional: populate only if event is Follow
21
*Profile
22
-
*FollowStats
23
-
*FollowStatus
24
25
// optional: populate only if event is Repo
26
IsStarred bool
···
211
return nil, err
212
}
213
214
-
var followStatuses map[string]FollowStatus
215
if loggedInUserDid != "" {
216
followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects)
217
if err != nil {
···
224
profile, _ := profiles[f.SubjectDid]
225
followStatMap, _ := followStatMap[f.SubjectDid]
226
227
-
followStatus := IsNotFollowing
228
if followStatuses != nil {
229
followStatus = followStatuses[f.SubjectDid]
230
}
···
5
"time"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/appview/models"
9
)
10
11
type TimelineEvent struct {
12
*Repo
13
+
*models.Follow
14
*Star
15
16
EventAt time.Time
···
20
21
// optional: populate only if event is Follow
22
*Profile
23
+
*models.FollowStats
24
+
*models.FollowStatus
25
26
// optional: populate only if event is Repo
27
IsStarred bool
···
212
return nil, err
213
}
214
215
+
var followStatuses map[string]models.FollowStatus
216
if loggedInUserDid != "" {
217
followStatuses, err = GetFollowStatuses(e, loggedInUserDid, subjects)
218
if err != nil {
···
225
profile, _ := profiles[f.SubjectDid]
226
followStatMap, _ := followStatMap[f.SubjectDid]
227
228
+
followStatus := models.IsNotFollowing
229
if followStatuses != nil {
230
followStatus = followStatuses[f.SubjectDid]
231
}
+1
-1
appview/ingester.go
+1
-1
appview/ingester.go
+38
appview/models/follow.go
+38
appview/models/follow.go
···
···
1
+
package models
2
+
3
+
import (
4
+
"time"
5
+
)
6
+
7
+
type Follow struct {
8
+
UserDid string
9
+
SubjectDid string
10
+
FollowedAt time.Time
11
+
Rkey string
12
+
}
13
+
14
+
type FollowStats struct {
15
+
Followers int64
16
+
Following int64
17
+
}
18
+
19
+
type FollowStatus int
20
+
21
+
const (
22
+
IsNotFollowing FollowStatus = iota
23
+
IsFollowing
24
+
IsSelf
25
+
)
26
+
27
+
func (s FollowStatus) String() string {
28
+
switch s {
29
+
case IsNotFollowing:
30
+
return "IsNotFollowing"
31
+
case IsFollowing:
32
+
return "IsFollowing"
33
+
case IsSelf:
34
+
return "IsSelf"
35
+
default:
36
+
return "IsNotFollowing"
37
+
}
38
+
}
+3
-2
appview/notify/merged_notifier.go
+3
-2
appview/notify/merged_notifier.go
···
4
"context"
5
6
"tangled.org/core/appview/db"
7
)
8
9
type mergedNotifier struct {
···
39
}
40
}
41
42
-
func (m *mergedNotifier) NewFollow(ctx context.Context, follow *db.Follow) {
43
for _, notifier := range m.notifiers {
44
notifier.NewFollow(ctx, follow)
45
}
46
}
47
-
func (m *mergedNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {
48
for _, notifier := range m.notifiers {
49
notifier.DeleteFollow(ctx, follow)
50
}
···
4
"context"
5
6
"tangled.org/core/appview/db"
7
+
"tangled.org/core/appview/models"
8
)
9
10
type mergedNotifier struct {
···
40
}
41
}
42
43
+
func (m *mergedNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
44
for _, notifier := range m.notifiers {
45
notifier.NewFollow(ctx, follow)
46
}
47
}
48
+
func (m *mergedNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
49
for _, notifier := range m.notifiers {
50
notifier.DeleteFollow(ctx, follow)
51
}
+5
-4
appview/notify/notifier.go
+5
-4
appview/notify/notifier.go
···
4
"context"
5
6
"tangled.org/core/appview/db"
7
)
8
9
type Notifier interface {
···
14
15
NewIssue(ctx context.Context, issue *db.Issue)
16
17
-
NewFollow(ctx context.Context, follow *db.Follow)
18
-
DeleteFollow(ctx context.Context, follow *db.Follow)
19
20
NewPull(ctx context.Context, pull *db.Pull)
21
NewPullComment(ctx context.Context, comment *db.PullComment)
···
39
40
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {}
41
42
-
func (m *BaseNotifier) NewFollow(ctx context.Context, follow *db.Follow) {}
43
-
func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {}
44
45
func (m *BaseNotifier) NewPull(ctx context.Context, pull *db.Pull) {}
46
func (m *BaseNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) {}
···
4
"context"
5
6
"tangled.org/core/appview/db"
7
+
"tangled.org/core/appview/models"
8
)
9
10
type Notifier interface {
···
15
16
NewIssue(ctx context.Context, issue *db.Issue)
17
18
+
NewFollow(ctx context.Context, follow *models.Follow)
19
+
DeleteFollow(ctx context.Context, follow *models.Follow)
20
21
NewPull(ctx context.Context, pull *db.Pull)
22
NewPullComment(ctx context.Context, comment *db.PullComment)
···
40
41
func (m *BaseNotifier) NewIssue(ctx context.Context, issue *db.Issue) {}
42
43
+
func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {}
44
+
func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {}
45
46
func (m *BaseNotifier) NewPull(ctx context.Context, pull *db.Pull) {}
47
func (m *BaseNotifier) NewPullComment(ctx context.Context, comment *db.PullComment) {}
+3
-3
appview/pages/pages.go
+3
-3
appview/pages/pages.go
···
411
type ProfileCard struct {
412
UserDid string
413
UserHandle string
414
-
FollowStatus db.FollowStatus
415
Punchcard *db.Punchcard
416
Profile *db.Profile
417
Stats ProfileStats
···
489
490
type FollowCard struct {
491
UserDid string
492
-
FollowStatus db.FollowStatus
493
FollowersCount int64
494
FollowingCount int64
495
Profile *db.Profile
···
521
522
type FollowFragmentParams struct {
523
UserDid string
524
-
FollowStatus db.FollowStatus
525
}
526
527
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
···
411
type ProfileCard struct {
412
UserDid string
413
UserHandle string
414
+
FollowStatus models.FollowStatus
415
Punchcard *db.Punchcard
416
Profile *db.Profile
417
Stats ProfileStats
···
489
490
type FollowCard struct {
491
UserDid string
492
+
FollowStatus models.FollowStatus
493
FollowersCount int64
494
FollowingCount int64
495
Profile *db.Profile
···
521
522
type FollowFragmentParams struct {
523
UserDid string
524
+
FollowStatus models.FollowStatus
525
}
526
527
func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error {
+3
-2
appview/posthog/notifier.go
+3
-2
appview/posthog/notifier.go
···
6
7
"github.com/posthog/posthog-go"
8
"tangled.org/core/appview/db"
9
"tangled.org/core/appview/notify"
10
)
11
···
98
}
99
}
100
101
-
func (n *posthogNotifier) NewFollow(ctx context.Context, follow *db.Follow) {
102
err := n.client.Enqueue(posthog.Capture{
103
DistinctId: follow.UserDid,
104
Event: "follow",
···
109
}
110
}
111
112
-
func (n *posthogNotifier) DeleteFollow(ctx context.Context, follow *db.Follow) {
113
err := n.client.Enqueue(posthog.Capture{
114
DistinctId: follow.UserDid,
115
Event: "unfollow",
···
6
7
"github.com/posthog/posthog-go"
8
"tangled.org/core/appview/db"
9
+
"tangled.org/core/appview/models"
10
"tangled.org/core/appview/notify"
11
)
12
···
99
}
100
}
101
102
+
func (n *posthogNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
103
err := n.client.Enqueue(posthog.Capture{
104
DistinctId: follow.UserDid,
105
Event: "follow",
···
110
}
111
}
112
113
+
func (n *posthogNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
114
err := n.client.Enqueue(posthog.Capture{
115
DistinctId: follow.UserDid,
116
Event: "unfollow",
+4
-3
appview/state/follow.go
+4
-3
appview/state/follow.go
···
9
lexutil "github.com/bluesky-social/indigo/lex/util"
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/tid"
14
)
···
59
60
log.Println("created atproto record: ", resp.Uri)
61
62
-
follow := &db.Follow{
63
UserDid: currentUser.Did,
64
SubjectDid: subjectIdent.DID.String(),
65
Rkey: rkey,
···
75
76
s.pages.FollowFragment(w, pages.FollowFragmentParams{
77
UserDid: subjectIdent.DID.String(),
78
-
FollowStatus: db.IsFollowing,
79
})
80
81
return
···
106
107
s.pages.FollowFragment(w, pages.FollowFragmentParams{
108
UserDid: subjectIdent.DID.String(),
109
-
FollowStatus: db.IsNotFollowing,
110
})
111
112
s.notifier.DeleteFollow(r.Context(), follow)
···
9
lexutil "github.com/bluesky-social/indigo/lex/util"
10
"tangled.org/core/api/tangled"
11
"tangled.org/core/appview/db"
12
+
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pages"
14
"tangled.org/core/tid"
15
)
···
60
61
log.Println("created atproto record: ", resp.Uri)
62
63
+
follow := &models.Follow{
64
UserDid: currentUser.Did,
65
SubjectDid: subjectIdent.DID.String(),
66
Rkey: rkey,
···
76
77
s.pages.FollowFragment(w, pages.FollowFragmentParams{
78
UserDid: subjectIdent.DID.String(),
79
+
FollowStatus: models.IsFollowing,
80
})
81
82
return
···
107
108
s.pages.FollowFragment(w, pages.FollowFragmentParams{
109
UserDid: subjectIdent.DID.String(),
110
+
FollowStatus: models.IsNotFollowing,
111
})
112
113
s.notifier.DeleteFollow(r.Context(), follow)
+9
-8
appview/state/profile.go
+9
-8
appview/state/profile.go
···
17
"github.com/gorilla/feeds"
18
"tangled.org/core/api/tangled"
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/pages"
21
)
22
···
76
}
77
78
loggedInUser := s.oauth.GetUser(r)
79
-
followStatus := db.IsNotFollowing
80
if loggedInUser != nil {
81
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did)
82
}
···
271
272
func (s *State) followPage(
273
r *http.Request,
274
-
fetchFollows func(db.Execer, string) ([]db.Follow, error),
275
-
extractDid func(db.Follow) string,
276
) (*FollowsPageParams, error) {
277
l := s.logger.With("handler", "reposPage")
278
···
329
followCards := make([]pages.FollowCard, len(follows))
330
for i, did := range followDids {
331
followStats := followStatsMap[did]
332
-
followStatus := db.IsNotFollowing
333
if _, exists := loggedInUserFollowing[did]; exists {
334
-
followStatus = db.IsFollowing
335
} else if loggedInUser != nil && loggedInUser.Did == did {
336
-
followStatus = db.IsSelf
337
}
338
339
var profile *db.Profile
···
358
}
359
360
func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
361
-
followPage, err := s.followPage(r, db.GetFollowers, func(f db.Follow) string { return f.UserDid })
362
if err != nil {
363
s.pages.Notice(w, "all-followers", "Failed to load followers")
364
return
···
372
}
373
374
func (s *State) followingPage(w http.ResponseWriter, r *http.Request) {
375
-
followPage, err := s.followPage(r, db.GetFollowing, func(f db.Follow) string { return f.SubjectDid })
376
if err != nil {
377
s.pages.Notice(w, "all-following", "Failed to load following")
378
return
···
17
"github.com/gorilla/feeds"
18
"tangled.org/core/api/tangled"
19
"tangled.org/core/appview/db"
20
+
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
)
23
···
77
}
78
79
loggedInUser := s.oauth.GetUser(r)
80
+
followStatus := models.IsNotFollowing
81
if loggedInUser != nil {
82
followStatus = db.GetFollowStatus(s.db, loggedInUser.Did, did)
83
}
···
272
273
func (s *State) followPage(
274
r *http.Request,
275
+
fetchFollows func(db.Execer, string) ([]models.Follow, error),
276
+
extractDid func(models.Follow) string,
277
) (*FollowsPageParams, error) {
278
l := s.logger.With("handler", "reposPage")
279
···
330
followCards := make([]pages.FollowCard, len(follows))
331
for i, did := range followDids {
332
followStats := followStatsMap[did]
333
+
followStatus := models.IsNotFollowing
334
if _, exists := loggedInUserFollowing[did]; exists {
335
+
followStatus = models.IsFollowing
336
} else if loggedInUser != nil && loggedInUser.Did == did {
337
+
followStatus = models.IsSelf
338
}
339
340
var profile *db.Profile
···
359
}
360
361
func (s *State) followersPage(w http.ResponseWriter, r *http.Request) {
362
+
followPage, err := s.followPage(r, db.GetFollowers, func(f models.Follow) string { return f.UserDid })
363
if err != nil {
364
s.pages.Notice(w, "all-followers", "Failed to load followers")
365
return
···
373
}
374
375
func (s *State) followingPage(w http.ResponseWriter, r *http.Request) {
376
+
followPage, err := s.followPage(r, db.GetFollowing, func(f models.Follow) string { return f.SubjectDid })
377
if err != nil {
378
s.pages.Notice(w, "all-following", "Failed to load following")
379
return