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 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
··· 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
··· 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
··· 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
··· 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
··· 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