+1
-1
appview/db/db.go
+1
-1
appview/db/db.go
···
59
59
create table if not exists follows (
60
60
user_did text not null,
61
61
subject_did text not null,
62
-
at_uri text not null,
62
+
rkey text not null,
63
63
followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
64
64
primary key (user_did, subject_did),
65
65
check (user_did <> subject_did)
+46
-2
appview/db/follow.go
+46
-2
appview/db/follow.go
···
20
20
21
21
// Get a follow record
22
22
func (d *DB) GetFollow(userDid, subjectDid string) (*Follow, error) {
23
-
query := `select user_did, subject_did, followed_at, at_uri from follows where user_did = ? and subject_did = ?`
23
+
query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?`
24
24
row := d.db.QueryRow(query, userDid, subjectDid)
25
25
26
26
var follow Follow
···
47
47
return err
48
48
}
49
49
50
+
func (d *DB) GetFollowerFollowing(did string) (int, int, error) {
51
+
followers, following := 0, 0
52
+
err := d.db.QueryRow(
53
+
`SELECT
54
+
COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
55
+
COUNT(CASE WHEN user_did = ? THEN 1 END) AS following
56
+
FROM follows;`, did, did).Scan(&followers, &following)
57
+
if err != nil {
58
+
return 0, 0, err
59
+
}
60
+
return followers, following, nil
61
+
}
62
+
63
+
type FollowStatus int
64
+
65
+
const (
66
+
IsNotFollowing FollowStatus = iota
67
+
IsFollowing
68
+
IsSelf
69
+
)
70
+
71
+
func (s FollowStatus) String() string {
72
+
switch s {
73
+
case IsNotFollowing:
74
+
return "IsNotFollowing"
75
+
case IsFollowing:
76
+
return "IsFollowing"
77
+
case IsSelf:
78
+
return "IsSelf"
79
+
default:
80
+
return "IsNotFollowing"
81
+
}
82
+
}
83
+
84
+
func (d *DB) GetFollowStatus(userDid, subjectDid string) FollowStatus {
85
+
if userDid == subjectDid {
86
+
return IsSelf
87
+
} else if _, err := d.GetFollow(userDid, subjectDid); err != nil {
88
+
return IsNotFollowing
89
+
} else {
90
+
return IsFollowing
91
+
}
92
+
}
93
+
50
94
func (d *DB) GetAllFollows() ([]Follow, error) {
51
95
var follows []Follow
52
96
53
-
rows, err := d.db.Query(`select user_did, subject_did, followed_at, at_uri from follows`)
97
+
rows, err := d.db.Query(`select user_did, subject_did, followed_at, rkey from follows`)
54
98
if err != nil {
55
99
return nil, err
56
100
}
+19
-17
appview/db/repos.go
+19
-17
appview/db/repos.go
···
9
9
Did string
10
10
Name string
11
11
Knot string
12
-
Created *time.Time
13
12
Rkey string
13
+
Created *time.Time
14
14
}
15
15
16
16
func (d *DB) GetAllRepos() ([]Repo, error) {
17
17
var repos []Repo
18
18
19
-
rows, err := d.db.Query(`select did, name, knot, created from repos`)
19
+
rows, err := d.db.Query(`select * from repos`)
20
20
if err != nil {
21
21
return nil, err
22
22
}
23
23
defer rows.Close()
24
24
25
25
for rows.Next() {
26
-
repo, err := scanRepo(rows)
26
+
var repo Repo
27
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
27
28
if err != nil {
28
29
return nil, err
29
30
}
30
-
repos = append(repos, *repo)
31
+
repos = append(repos, repo)
31
32
}
32
33
33
34
if err := rows.Err(); err != nil {
···
47
48
defer rows.Close()
48
49
49
50
for rows.Next() {
50
-
repo, err := scanRepo(rows)
51
+
var repo Repo
52
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
51
53
if err != nil {
52
54
return nil, err
53
55
}
54
-
repos = append(repos, *repo)
56
+
repos = append(repos, repo)
55
57
}
56
58
57
59
if err := rows.Err(); err != nil {
···
97
99
func (d *DB) CollaboratingIn(collaborator string) ([]Repo, error) {
98
100
var repos []Repo
99
101
100
-
rows, err := d.db.Query(`select r.* from repos r join collaborators c on r.id = c.repo where c.did = ?;`, collaborator)
102
+
rows, err := d.db.Query(`select r.did, r.name, r.knot, r.rkey, r.created from repos r join collaborators c on r.id = c.repo where c.did = ?;`, collaborator)
101
103
if err != nil {
102
104
return nil, err
103
105
}
104
106
defer rows.Close()
105
107
106
108
for rows.Next() {
107
-
repo, err := scanRepo(rows)
109
+
var repo Repo
110
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
108
111
if err != nil {
109
112
return nil, err
110
113
}
111
-
repos = append(repos, *repo)
114
+
repos = append(repos, repo)
112
115
}
113
116
114
117
if err := rows.Err(); err != nil {
···
118
121
return repos, nil
119
122
}
120
123
121
-
func scanRepo(rows *sql.Rows) (*Repo, error) {
122
-
var repo Repo
124
+
func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error {
123
125
var createdAt string
124
-
if err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt); err != nil {
125
-
return nil, err
126
+
if err := rows.Scan(did, name, knot, rkey, &createdAt); err != nil {
127
+
return err
126
128
}
127
129
128
130
createdAtTime, err := time.Parse(time.RFC3339, createdAt)
129
131
if err != nil {
130
132
now := time.Now()
131
-
repo.Created = &now
133
+
created = &now
134
+
} else {
135
+
created = &createdAtTime
132
136
}
133
137
134
-
repo.Created = &createdAtTime
135
-
136
-
return &repo, nil
138
+
return nil
137
139
}
+7
appview/pages/pages.go
+7
appview/pages/pages.go
···
226
226
UserHandle string
227
227
Repos []db.Repo
228
228
CollaboratingRepos []db.Repo
229
+
ProfileStats ProfileStats
230
+
FollowStatus db.FollowStatus
231
+
}
232
+
233
+
type ProfileStats struct {
234
+
Followers int
235
+
Following int
229
236
}
230
237
231
238
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
+27
-3
appview/pages/templates/user/profile.html
+27
-3
appview/pages/templates/user/profile.html
···
1
1
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
2
2
3
3
{{ define "content" }}
4
-
<h1>{{ didOrHandle .UserDid .UserHandle }}</h1>
4
+
<div class="flex ">
5
+
<h1 class="pb-1">
6
+
{{ didOrHandle .UserDid .UserHandle }}
7
+
</h1>
8
+
{{ if ne .FollowStatus.String "IsSelf" }}
9
+
<button id="followBtn"
10
+
class="btn mt-2"
11
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}
12
+
hx-post="/follow?subject={{.UserDid}}"
13
+
{{ else }}
14
+
hx-delete="/follow?subject={{.UserDid}}"
15
+
{{ end }}
16
+
hx-trigger="click"
17
+
hx-target="#followBtn"
18
+
hx-swap="outerHTML"
19
+
>
20
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
21
+
</button>
22
+
{{ end }}
23
+
</div>
24
+
<div class="text-sm mb-4">
25
+
<span>{{ .ProfileStats.Followers }} followers</span>
26
+
<div class="inline-block px-1 select-none after:content-['·']"></div>
27
+
<span>following {{ .ProfileStats.Following }}</span>
28
+
</div>
5
29
<p class="text-xs font-bold py-2">REPOS</p>
6
30
<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
7
31
{{ range .Repos }}
···
33
57
class="border border-black p-4 shadow-sm bg-white"
34
58
>
35
59
<div id="repo-card-name" class="font-medium">
36
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}">
37
-
@{{ or $.UserHandle $.UserDid }}/{{ .Name }}
60
+
<a href="/{{ .Did }}/{{ .Name }}">
61
+
@{{ .Did }}/{{ .Name }}
38
62
</a>
39
63
</div>
40
64
<div
+40
-6
appview/state/state.go
+40
-6
appview/state/state.go
···
78
78
record := tangled.GraphFollow{}
79
79
err := json.Unmarshal(raw, &record)
80
80
if err != nil {
81
+
log.Println("invalid record")
81
82
return err
82
83
}
83
84
err = db.AddFollow(did, record.Subject, e.Commit.RKey)
···
619
620
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
620
621
}
621
622
623
+
followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
624
+
if err != nil {
625
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
626
+
}
627
+
628
+
loggedInUser := s.auth.GetUser(r)
629
+
followStatus := db.IsNotFollowing
630
+
if loggedInUser != nil {
631
+
followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
632
+
}
633
+
622
634
s.pages.ProfilePage(w, pages.ProfilePageParams{
623
-
LoggedInUser: s.auth.GetUser(r),
635
+
LoggedInUser: loggedInUser,
624
636
UserDid: ident.DID.String(),
625
637
UserHandle: ident.Handle.String(),
626
638
Repos: repos,
627
639
CollaboratingRepos: collaboratingRepos,
640
+
ProfileStats: pages.ProfileStats{
641
+
Followers: followers,
642
+
Following: following,
643
+
},
644
+
FollowStatus: db.FollowStatus(followStatus),
628
645
})
629
646
}
630
647
···
676
693
677
694
log.Println("created atproto record: ", resp.Uri)
678
695
696
+
w.Write([]byte(fmt.Sprintf(`
697
+
<button id="followBtn"
698
+
class="btn mt-2"
699
+
hx-delete="/follow?subject=%s"
700
+
hx-trigger="click"
701
+
hx-target="#followBtn"
702
+
hx-swap="outerHTML">
703
+
Unfollow
704
+
</button>
705
+
`, subjectIdent.DID.String())))
706
+
679
707
return
680
708
case http.MethodDelete:
681
709
// find the record in the db
682
-
683
710
follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
684
711
if err != nil {
685
712
log.Println("failed to get follow relationship")
686
713
return
687
714
}
688
715
689
-
resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
716
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
690
717
Collection: tangled.GraphFollowNSID,
691
718
Repo: currentUser.Did,
692
719
Rkey: follow.RKey,
693
720
})
694
-
695
-
log.Println(resp.Commit.Cid)
696
721
697
722
if err != nil {
698
723
log.Println("failed to unfollow")
···
705
730
// this is not an issue, the firehose event might have already done this
706
731
}
707
732
708
-
w.WriteHeader(http.StatusNoContent)
733
+
w.Write([]byte(fmt.Sprintf(`
734
+
<button id="followBtn"
735
+
class="btn mt-2"
736
+
hx-post="/follow?subject=%s"
737
+
hx-trigger="click"
738
+
hx-target="#followBtn"
739
+
hx-swap="outerHTML">
740
+
Follow
741
+
</button>
742
+
`, subjectIdent.DID.String())))
709
743
return
710
744
}
711
745