forked from tangled.org/core
this repo has no description

add followers/following to UI

Changed files
+140 -29
appview
db
pages
templates
state
+1 -1
appview/db/db.go
···
create table if not exists follows (
user_did text not null,
subject_did text not null,
-
at_uri text not null,
+
rkey text not null,
followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
primary key (user_did, subject_did),
check (user_did <> subject_did)
+46 -2
appview/db/follow.go
···
// Get a follow record
func (d *DB) GetFollow(userDid, subjectDid string) (*Follow, error) {
-
query := `select user_did, subject_did, followed_at, at_uri from follows where user_did = ? and subject_did = ?`
+
query := `select user_did, subject_did, followed_at, rkey from follows where user_did = ? and subject_did = ?`
row := d.db.QueryRow(query, userDid, subjectDid)
var follow Follow
···
return err
}
+
func (d *DB) GetFollowerFollowing(did string) (int, int, error) {
+
followers, following := 0, 0
+
err := d.db.QueryRow(
+
`SELECT
+
COUNT(CASE WHEN subject_did = ? THEN 1 END) AS followers,
+
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 followers, following, nil
+
}
+
+
type FollowStatus int
+
+
const (
+
IsNotFollowing FollowStatus = iota
+
IsFollowing
+
IsSelf
+
)
+
+
func (s FollowStatus) String() string {
+
switch s {
+
case IsNotFollowing:
+
return "IsNotFollowing"
+
case IsFollowing:
+
return "IsFollowing"
+
case IsSelf:
+
return "IsSelf"
+
default:
+
return "IsNotFollowing"
+
}
+
}
+
+
func (d *DB) GetFollowStatus(userDid, subjectDid string) FollowStatus {
+
if userDid == subjectDid {
+
return IsSelf
+
} else if _, err := d.GetFollow(userDid, subjectDid); err != nil {
+
return IsNotFollowing
+
} else {
+
return IsFollowing
+
}
+
}
+
func (d *DB) GetAllFollows() ([]Follow, error) {
var follows []Follow
-
rows, err := d.db.Query(`select user_did, subject_did, followed_at, at_uri from follows`)
+
rows, err := d.db.Query(`select user_did, subject_did, followed_at, rkey from follows`)
if err != nil {
return nil, err
}
+19 -17
appview/db/repos.go
···
Did string
Name string
Knot string
-
Created *time.Time
Rkey string
+
Created *time.Time
}
func (d *DB) GetAllRepos() ([]Repo, error) {
var repos []Repo
-
rows, err := d.db.Query(`select did, name, knot, created from repos`)
+
rows, err := d.db.Query(`select * from repos`)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
-
repo, err := scanRepo(rows)
+
var repo Repo
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
if err != nil {
return nil, err
}
-
repos = append(repos, *repo)
+
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
···
defer rows.Close()
for rows.Next() {
-
repo, err := scanRepo(rows)
+
var repo Repo
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
if err != nil {
return nil, err
}
-
repos = append(repos, *repo)
+
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
···
func (d *DB) CollaboratingIn(collaborator string) ([]Repo, error) {
var repos []Repo
-
rows, err := d.db.Query(`select r.* from repos r join collaborators c on r.id = c.repo where c.did = ?;`, collaborator)
+
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)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
-
repo, err := scanRepo(rows)
+
var repo Repo
+
err := scanRepo(rows, &repo.Did, &repo.Name, &repo.Knot, &repo.Rkey, repo.Created)
if err != nil {
return nil, err
}
-
repos = append(repos, *repo)
+
repos = append(repos, repo)
}
if err := rows.Err(); err != nil {
···
return repos, nil
}
-
func scanRepo(rows *sql.Rows) (*Repo, error) {
-
var repo Repo
+
func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error {
var createdAt string
-
if err := rows.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt); err != nil {
-
return nil, err
+
if err := rows.Scan(did, name, knot, rkey, &createdAt); err != nil {
+
return err
}
createdAtTime, err := time.Parse(time.RFC3339, createdAt)
if err != nil {
now := time.Now()
-
repo.Created = &now
+
created = &now
+
} else {
+
created = &createdAtTime
}
-
repo.Created = &createdAtTime
-
-
return &repo, nil
+
return nil
}
+7
appview/pages/pages.go
···
UserHandle string
Repos []db.Repo
CollaboratingRepos []db.Repo
+
ProfileStats ProfileStats
+
FollowStatus db.FollowStatus
+
}
+
+
type ProfileStats struct {
+
Followers int
+
Following int
}
func (p *Pages) ProfilePage(w io.Writer, params ProfilePageParams) error {
+27 -3
appview/pages/templates/user/profile.html
···
{{ define "title" }}{{ or .UserHandle .UserDid }}{{ end }}
{{ define "content" }}
-
<h1>{{ didOrHandle .UserDid .UserHandle }}</h1>
+
<div class="flex ">
+
<h1 class="pb-1">
+
{{ didOrHandle .UserDid .UserHandle }}
+
</h1>
+
{{ if ne .FollowStatus.String "IsSelf" }}
+
<button id="followBtn"
+
class="btn mt-2"
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}
+
hx-post="/follow?subject={{.UserDid}}"
+
{{ else }}
+
hx-delete="/follow?subject={{.UserDid}}"
+
{{ end }}
+
hx-trigger="click"
+
hx-target="#followBtn"
+
hx-swap="outerHTML"
+
>
+
{{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }}
+
</button>
+
{{ end }}
+
</div>
+
<div class="text-sm mb-4">
+
<span>{{ .ProfileStats.Followers }} followers</span>
+
<div class="inline-block px-1 select-none after:content-['·']"></div>
+
<span>following {{ .ProfileStats.Following }}</span>
+
</div>
<p class="text-xs font-bold py-2">REPOS</p>
<div id="repos" class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
{{ range .Repos }}
···
class="border border-black p-4 shadow-sm bg-white"
>
<div id="repo-card-name" class="font-medium">
-
<a href="/@{{ or $.UserHandle $.UserDid }}/{{ .Name }}">
-
@{{ or $.UserHandle $.UserDid }}/{{ .Name }}
+
<a href="/{{ .Did }}/{{ .Name }}">
+
@{{ .Did }}/{{ .Name }}
</a>
</div>
<div
+40 -6
appview/state/state.go
···
record := tangled.GraphFollow{}
err := json.Unmarshal(raw, &record)
if err != nil {
+
log.Println("invalid record")
return err
}
err = db.AddFollow(did, record.Subject, e.Commit.RKey)
···
log.Printf("getting collaborating repos for %s: %s", ident.DID.String(), err)
}
+
followers, following, err := s.db.GetFollowerFollowing(ident.DID.String())
+
if err != nil {
+
log.Printf("getting follow stats repos for %s: %s", ident.DID.String(), err)
+
}
+
+
loggedInUser := s.auth.GetUser(r)
+
followStatus := db.IsNotFollowing
+
if loggedInUser != nil {
+
followStatus = s.db.GetFollowStatus(loggedInUser.Did, ident.DID.String())
+
}
+
s.pages.ProfilePage(w, pages.ProfilePageParams{
-
LoggedInUser: s.auth.GetUser(r),
+
LoggedInUser: loggedInUser,
UserDid: ident.DID.String(),
UserHandle: ident.Handle.String(),
Repos: repos,
CollaboratingRepos: collaboratingRepos,
+
ProfileStats: pages.ProfileStats{
+
Followers: followers,
+
Following: following,
+
},
+
FollowStatus: db.FollowStatus(followStatus),
})
}
···
log.Println("created atproto record: ", resp.Uri)
+
w.Write([]byte(fmt.Sprintf(`
+
<button id="followBtn"
+
class="btn mt-2"
+
hx-delete="/follow?subject=%s"
+
hx-trigger="click"
+
hx-target="#followBtn"
+
hx-swap="outerHTML">
+
Unfollow
+
</button>
+
`, subjectIdent.DID.String())))
+
return
case http.MethodDelete:
// find the record in the db
-
follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())
if err != nil {
log.Println("failed to get follow relationship")
return
}
-
resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
+
_, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{
Collection: tangled.GraphFollowNSID,
Repo: currentUser.Did,
Rkey: follow.RKey,
})
-
-
log.Println(resp.Commit.Cid)
if err != nil {
log.Println("failed to unfollow")
···
// this is not an issue, the firehose event might have already done this
}
-
w.WriteHeader(http.StatusNoContent)
+
w.Write([]byte(fmt.Sprintf(`
+
<button id="followBtn"
+
class="btn mt-2"
+
hx-post="/follow?subject=%s"
+
hx-trigger="click"
+
hx-target="#followBtn"
+
hx-swap="outerHTML">
+
Follow
+
</button>
+
`, subjectIdent.DID.String())))
return
}