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