Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2 (Please be gentle).

add stars to tangled

+691 -196
api/tangled/cbor_gen.go

This is a binary file and will not be displayed.

api/tangled/feedstar.go

This is a binary file and will not be displayed.

+10
appview/db/db.go
··· 112 112 next_issue_id integer not null default 1 113 113 ); 114 114 115 + create table if not exists stars ( 116 + id integer primary key autoincrement, 117 + starred_by_did text not null, 118 + repo_at text not null, 119 + rkey text not null, 120 + created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), 121 + foreign key (repo_at) references repos(at_uri) on delete cascade, 122 + unique(starred_by_did, repo_at) 123 + ); 124 + 115 125 `) 116 126 if err != nil { 117 127 return nil, err
+11 -6
appview/db/follow.go
··· 9 9 UserDid string 10 10 SubjectDid string 11 11 FollowedAt time.Time 12 - RKey string 12 + Rkey string 13 13 } 14 14 15 15 func AddFollow(e Execer, userDid, subjectDid, rkey string) error { ··· 25 25 26 26 var follow Follow 27 27 var followedAt string 28 - err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.RKey) 28 + err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey) 29 29 if err != nil { 30 30 return nil, err 31 31 } ··· 41 41 return &follow, nil 42 42 } 43 43 44 - // Get a follow record 44 + // Remove a follow 45 45 func DeleteFollow(e Execer, userDid, subjectDid string) error { 46 46 _, err := e.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid) 47 47 return err ··· 91 91 } 92 92 } 93 93 94 - func GetAllFollows(e Execer) ([]Follow, error) { 94 + func GetAllFollows(e Execer, limit int) ([]Follow, error) { 95 95 var follows []Follow 96 96 97 - rows, err := e.Query(`select user_did, subject_did, followed_at, rkey from follows`) 97 + rows, err := e.Query(` 98 + select user_did, subject_did, followed_at, rkey 99 + from follows 100 + order by followed_at desc 101 + limit ?`, limit, 102 + ) 98 103 if err != nil { 99 104 return nil, err 100 105 } ··· 108 103 for rows.Next() { 109 104 var follow Follow 110 105 var followedAt string 111 - if err := rows.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.RKey); err != nil { 106 + if err := rows.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.Rkey); err != nil { 112 107 return nil, err 113 108 } 114 109
+37 -12
appview/db/issues.go
··· 3 3 import ( 4 4 "database/sql" 5 5 "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 6 8 ) 7 9 8 10 type Issue struct { 9 - RepoAt string 11 + RepoAt syntax.ATURI 10 12 OwnerDid string 11 13 IssueId int 12 14 IssueAt string ··· 20 18 21 19 type Comment struct { 22 20 OwnerDid string 23 - RepoAt string 21 + RepoAt syntax.ATURI 24 22 CommentAt string 25 23 Issue int 26 24 CommentId int ··· 67 65 return nil 68 66 } 69 67 70 - func SetIssueAt(e Execer, repoAt string, issueId int, issueAt string) error { 68 + func SetIssueAt(e Execer, repoAt syntax.ATURI, issueId int, issueAt string) error { 71 69 _, err := e.Exec(`update issues set issue_at = ? where repo_at = ? and issue_id = ?`, issueAt, repoAt, issueId) 72 70 return err 73 71 } 74 72 75 - func GetIssueAt(e Execer, repoAt string, issueId int) (string, error) { 73 + func GetIssueAt(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 76 74 var issueAt string 77 75 err := e.QueryRow(`select issue_at from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&issueAt) 78 76 return issueAt, err 79 77 } 80 78 81 - func GetIssueId(e Execer, repoAt string) (int, error) { 79 + func GetIssueId(e Execer, repoAt syntax.ATURI) (int, error) { 82 80 var issueId int 83 81 err := e.QueryRow(`select next_issue_id from repo_issue_seqs where repo_at = ?`, repoAt).Scan(&issueId) 84 82 return issueId - 1, err 85 83 } 86 84 87 - func GetIssueOwnerDid(e Execer, repoAt string, issueId int) (string, error) { 85 + func GetIssueOwnerDid(e Execer, repoAt syntax.ATURI, issueId int) (string, error) { 88 86 var ownerDid string 89 87 err := e.QueryRow(`select owner_did from issues where repo_at = ? and issue_id = ?`, repoAt, issueId).Scan(&ownerDid) 90 88 return ownerDid, err 91 89 } 92 90 93 - func GetIssues(e Execer, repoAt string) ([]Issue, error) { 91 + func GetIssues(e Execer, repoAt syntax.ATURI) ([]Issue, error) { 94 92 var issues []Issue 95 93 96 94 rows, err := e.Query(`select owner_did, issue_id, created, title, body, open from issues where repo_at = ? order by created desc`, repoAt) ··· 123 121 return issues, nil 124 122 } 125 123 126 - func GetIssue(e Execer, repoAt string, issueId int) (*Issue, error) { 124 + func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, error) { 127 125 query := `select owner_did, created, title, body, open from issues where repo_at = ? and issue_id = ?` 128 126 row := e.QueryRow(query, repoAt, issueId) 129 127 ··· 143 141 return &issue, nil 144 142 } 145 143 146 - func GetIssueWithComments(e Execer, repoAt string, issueId int) (*Issue, []Comment, error) { 144 + func GetIssueWithComments(e Execer, repoAt syntax.ATURI, issueId int) (*Issue, []Comment, error) { 147 145 query := `select owner_did, issue_id, created, title, body, open from issues where repo_at = ? and issue_id = ?` 148 146 row := e.QueryRow(query, repoAt, issueId) 149 147 ··· 182 180 return err 183 181 } 184 182 185 - func GetComments(e Execer, repoAt string, issueId int) ([]Comment, error) { 183 + func GetComments(e Execer, repoAt syntax.ATURI, issueId int) ([]Comment, error) { 186 184 var comments []Comment 187 185 188 186 rows, err := e.Query(`select owner_did, issue_id, comment_id, comment_at, body, created from comments where repo_at = ? and issue_id = ? order by created asc`, repoAt, issueId) ··· 218 216 return comments, nil 219 217 } 220 218 221 - func CloseIssue(e Execer, repoAt string, issueId int) error { 219 + func CloseIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 222 220 _, err := e.Exec(`update issues set open = 0 where repo_at = ? and issue_id = ?`, repoAt, issueId) 223 221 return err 224 222 } 225 223 226 - func ReopenIssue(e Execer, repoAt string, issueId int) error { 224 + func ReopenIssue(e Execer, repoAt syntax.ATURI, issueId int) error { 227 225 _, err := e.Exec(`update issues set open = 1 where repo_at = ? and issue_id = ?`, repoAt, issueId) 228 226 return err 227 + } 228 + 229 + type IssueCount struct { 230 + Open int 231 + Closed int 232 + } 233 + 234 + func GetIssueCount(e Execer, repoAt syntax.ATURI) (IssueCount, error) { 235 + row := e.QueryRow(` 236 + select 237 + count(case when open = 1 then 1 end) as open_count, 238 + count(case when open = 0 then 1 end) as closed_count 239 + from issues 240 + where repo_at = ?`, 241 + repoAt, 242 + ) 243 + 244 + var count IssueCount 245 + if err := row.Scan(&count.Open, &count.Closed); err != nil { 246 + return IssueCount{0, 0}, err 247 + } 248 + 249 + return count, nil 229 250 }
+29 -2
appview/db/repos.go
··· 14 14 AtUri string 15 15 } 16 16 17 - func GetAllRepos(e Execer) ([]Repo, error) { 17 + func GetAllRepos(e Execer, limit int) ([]Repo, error) { 18 18 var repos []Repo 19 19 20 - rows, err := e.Query(`select did, name, knot, rkey, created from repos`) 20 + rows, err := e.Query( 21 + `select did, name, knot, rkey, created 22 + from repos 23 + order by created desc 24 + limit ? 25 + `, 26 + limit, 27 + ) 21 28 if err != nil { 22 29 return nil, err 23 30 } ··· 86 79 return &repo, nil 87 80 } 88 81 82 + func GetRepoByAtUri(e Execer, atUri string) (*Repo, error) { 83 + var repo Repo 84 + 85 + row := e.QueryRow(`select did, name, knot, created, at_uri from repos where at_uri = ?`, atUri) 86 + 87 + var createdAt string 88 + if err := row.Scan(&repo.Did, &repo.Name, &repo.Knot, &createdAt, &repo.AtUri); err != nil { 89 + return nil, err 90 + } 91 + createdAtTime, _ := time.Parse(time.RFC3339, createdAt) 92 + repo.Created = createdAtTime 93 + 94 + return &repo, nil 95 + } 96 + 89 97 func AddRepo(e Execer, repo *Repo) error { 90 98 _, err := e.Exec(`insert into repos (did, name, knot, rkey, at_uri) values (?, ?, ?, ?, ?)`, repo.Did, repo.Name, repo.Knot, repo.Rkey, repo.AtUri) 91 99 return err ··· 142 120 } 143 121 144 122 return repos, nil 123 + } 124 + 125 + type RepoStats struct { 126 + StarCount int 127 + IssueCount IssueCount 145 128 } 146 129 147 130 func scanRepo(rows *sql.Rows, did, name, knot, rkey *string, created *time.Time) error {
+150
appview/db/star.go
··· 1 + package db 2 + 3 + import ( 4 + "log" 5 + "time" 6 + 7 + "github.com/bluesky-social/indigo/atproto/syntax" 8 + ) 9 + 10 + type Star struct { 11 + StarredByDid string 12 + RepoAt syntax.ATURI 13 + Repo *Repo 14 + Created time.Time 15 + Rkey string 16 + } 17 + 18 + func (star *Star) ResolveRepo(e Execer) error { 19 + if star.Repo != nil { 20 + return nil 21 + } 22 + 23 + repo, err := GetRepoByAtUri(e, star.RepoAt.String()) 24 + if err != nil { 25 + return err 26 + } 27 + 28 + star.Repo = repo 29 + return nil 30 + } 31 + 32 + func AddStar(e Execer, starredByDid string, repoAt syntax.ATURI, rkey string) error { 33 + query := `insert or ignore into stars (starred_by_did, repo_at, rkey) values (?, ?, ?)` 34 + _, err := e.Exec(query, starredByDid, repoAt, rkey) 35 + return err 36 + } 37 + 38 + // Get a star record 39 + func GetStar(e Execer, starredByDid string, repoAt syntax.ATURI) (*Star, error) { 40 + query := ` 41 + select starred_by_did, repo_at, created, rkey 42 + from stars 43 + where starred_by_did = ? and repo_at = ?` 44 + row := e.QueryRow(query, starredByDid, repoAt) 45 + 46 + var star Star 47 + var created string 48 + err := row.Scan(&star.StarredByDid, &star.RepoAt, &created, &star.Rkey) 49 + if err != nil { 50 + return nil, err 51 + } 52 + 53 + createdAtTime, err := time.Parse(time.RFC3339, created) 54 + if err != nil { 55 + log.Println("unable to determine followed at time") 56 + star.Created = time.Now() 57 + } else { 58 + star.Created = createdAtTime 59 + } 60 + 61 + return &star, nil 62 + } 63 + 64 + // Remove a star 65 + func DeleteStar(e Execer, starredByDid string, repoAt syntax.ATURI) error { 66 + _, err := e.Exec(`delete from stars where starred_by_did = ? and repo_at = ?`, starredByDid, repoAt) 67 + return err 68 + } 69 + 70 + func GetStarCount(e Execer, repoAt syntax.ATURI) (int, error) { 71 + stars := 0 72 + err := e.QueryRow( 73 + `select count(starred_by_did) from stars where repo_at = ?`, repoAt).Scan(&stars) 74 + if err != nil { 75 + return 0, err 76 + } 77 + return stars, nil 78 + } 79 + 80 + func GetStarStatus(e Execer, userDid string, repoAt syntax.ATURI) bool { 81 + if _, err := GetStar(e, userDid, repoAt); err != nil { 82 + return false 83 + } else { 84 + return true 85 + } 86 + } 87 + 88 + func GetAllStars(e Execer, limit int) ([]Star, error) { 89 + var stars []Star 90 + 91 + rows, err := e.Query(` 92 + select 93 + s.starred_by_did, 94 + s.repo_at, 95 + s.rkey, 96 + s.created, 97 + r.did, 98 + r.name, 99 + r.knot, 100 + r.rkey, 101 + r.created, 102 + r.at_uri 103 + from stars s 104 + join repos r on s.repo_at = r.at_uri 105 + `) 106 + 107 + if err != nil { 108 + return nil, err 109 + } 110 + defer rows.Close() 111 + 112 + for rows.Next() { 113 + var star Star 114 + var repo Repo 115 + var starCreatedAt, repoCreatedAt string 116 + 117 + if err := rows.Scan( 118 + &star.StarredByDid, 119 + &star.RepoAt, 120 + &star.Rkey, 121 + &starCreatedAt, 122 + &repo.Did, 123 + &repo.Name, 124 + &repo.Knot, 125 + &repo.Rkey, 126 + &repoCreatedAt, 127 + &repo.AtUri, 128 + ); err != nil { 129 + return nil, err 130 + } 131 + 132 + star.Created, err = time.Parse(time.RFC3339, starCreatedAt) 133 + if err != nil { 134 + star.Created = time.Now() 135 + } 136 + repo.Created, err = time.Parse(time.RFC3339, repoCreatedAt) 137 + if err != nil { 138 + repo.Created = time.Now() 139 + } 140 + star.Repo = &repo 141 + 142 + stars = append(stars, star) 143 + } 144 + 145 + if err := rows.Err(); err != nil { 146 + return nil, err 147 + } 148 + 149 + return stars, nil 150 + }
+20 -6
appview/db/timeline.go
··· 8 8 type TimelineEvent struct { 9 9 *Repo 10 10 *Follow 11 + *Star 11 12 EventAt time.Time 12 13 } 13 14 15 + // TODO: this gathers heterogenous events from different sources and aggregates 16 + // them in code; if we did this entirely in sql, we could order and limit and paginate easily 14 17 func MakeTimeline(e Execer) ([]TimelineEvent, error) { 15 18 var events []TimelineEvent 19 + limit := 50 16 20 17 - repos, err := GetAllRepos(e) 21 + repos, err := GetAllRepos(e, limit) 18 22 if err != nil { 19 23 return nil, err 20 24 } 21 25 22 - follows, err := GetAllFollows(e) 26 + follows, err := GetAllFollows(e, limit) 27 + if err != nil { 28 + return nil, err 29 + } 30 + 31 + stars, err := GetAllStars(e, limit) 23 32 if err != nil { 24 33 return nil, err 25 34 } ··· 36 27 for _, repo := range repos { 37 28 events = append(events, TimelineEvent{ 38 29 Repo: &repo, 39 - Follow: nil, 40 30 EventAt: repo.Created, 41 31 }) 42 32 } 43 33 44 34 for _, follow := range follows { 45 35 events = append(events, TimelineEvent{ 46 - Repo: nil, 47 36 Follow: &follow, 48 37 EventAt: follow.FollowedAt, 38 + }) 39 + } 40 + 41 + for _, star := range stars { 42 + events = append(events, TimelineEvent{ 43 + Star: &star, 44 + EventAt: star.Created, 49 45 }) 50 46 } 51 47 ··· 59 45 }) 60 46 61 47 // Limit the slice to 100 events 62 - if len(events) > 50 { 63 - events = events[:50] 48 + if len(events) > limit { 49 + events = events[:limit] 64 50 } 65 51 66 52 return events, nil
+7
appview/pages/funcmap.go
··· 106 106 "markdown": func(text string) template.HTML { 107 107 return template.HTML(renderMarkdown(text)) 108 108 }, 109 + "isNil": func(t any) bool { 110 + // returns false for other "zero" values 111 + return t == nil 112 + }, 113 + "not": func(t bool) bool { 114 + return !t 115 + }, 109 116 } 110 117 }
+57 -2
appview/pages/pages.go
··· 17 17 chromahtml "github.com/alecthomas/chroma/v2/formatters/html" 18 18 "github.com/alecthomas/chroma/v2/lexers" 19 19 "github.com/alecthomas/chroma/v2/styles" 20 + "github.com/bluesky-social/indigo/atproto/syntax" 20 21 "github.com/microcosm-cc/bluemonday" 21 22 "github.com/sotangled/tangled/appview/auth" 22 23 "github.com/sotangled/tangled/appview/db" ··· 44 43 name := strings.TrimPrefix(path, "templates/") 45 44 name = strings.TrimSuffix(name, ".html") 46 45 47 - if !strings.HasPrefix(path, "templates/layouts/") { 46 + // add fragments as templates 47 + if strings.HasPrefix(path, "templates/fragments/") { 48 + tmpl, err := template.New(name). 49 + Funcs(funcMap()). 50 + ParseFS(files, path) 51 + if err != nil { 52 + return fmt.Errorf("setting up fragment: %w", err) 53 + } 54 + 55 + templates[name] = tmpl 56 + log.Printf("loaded fragment: %s", name) 57 + } 58 + 59 + // layouts and fragments are applied first 60 + if !strings.HasPrefix(path, "templates/layouts/") && 61 + !strings.HasPrefix(path, "templates/fragments/") { 48 62 // Add the page template on top of the base 49 63 tmpl, err := template.New(name). 50 64 Funcs(funcMap()). 51 - ParseFS(files, "templates/layouts/*.html", path) 65 + ParseFS(files, "templates/layouts/*.html", "templates/fragments/*.html", path) 52 66 if err != nil { 53 67 return fmt.Errorf("setting up template: %w", err) 54 68 } ··· 175 159 return p.execute("user/profile", w, params) 176 160 } 177 161 162 + type FollowFragmentParams struct { 163 + UserDid string 164 + FollowStatus db.FollowStatus 165 + } 166 + 167 + func (p *Pages) FollowFragment(w io.Writer, params FollowFragmentParams) error { 168 + return p.executePlain("fragments/follow", w, params) 169 + } 170 + 171 + type StarFragmentParams struct { 172 + IsStarred bool 173 + RepoAt syntax.ATURI 174 + Stats db.RepoStats 175 + } 176 + 177 + func (p *Pages) StarFragment(w io.Writer, params StarFragmentParams) error { 178 + return p.executePlain("fragments/star", w, params) 179 + } 180 + 178 181 type RepoInfo struct { 179 182 Name string 180 183 OwnerDid string 181 184 OwnerHandle string 182 185 Description string 183 186 Knot string 187 + RepoAt syntax.ATURI 184 188 SettingsAllowed bool 189 + IsStarred bool 190 + Stats db.RepoStats 185 191 } 186 192 187 193 func (r RepoInfo) OwnerWithAt() string { ··· 230 192 } 231 193 232 194 return tabs 195 + } 196 + 197 + // each tab on a repo could have some metadata: 198 + // 199 + // issues -> number of open issues etc. 200 + // settings -> a warning icon to setup branch protection? idk 201 + // 202 + // we gather these bits of info here, because go templates 203 + // are difficult to program in 204 + func (r RepoInfo) TabMetadata() map[string]any { 205 + meta := make(map[string]any) 206 + 207 + meta["issues"] = r.Stats.IssueCount.Open 208 + 209 + // more stuff? 210 + 211 + return meta 233 212 } 234 213 235 214 type RepoIndexParams struct {
+17
appview/pages/templates/fragments/follow.html
··· 1 + {{ define "fragments/follow" }} 2 + <button id="followBtn" 3 + class="btn mt-2 w-full" 4 + 5 + {{ if eq .FollowStatus.String "IsNotFollowing" }} 6 + hx-post="/follow?subject={{.UserDid}}" 7 + {{ else }} 8 + hx-delete="/follow?subject={{.UserDid}}" 9 + {{ end }} 10 + 11 + hx-trigger="click" 12 + hx-target="#followBtn" 13 + hx-swap="outerHTML" 14 + > 15 + {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }} 16 + </button> 17 + {{ end }}
+36
appview/pages/templates/fragments/star.html
··· 1 + {{ define "fragments/star" }} 2 + <button id="starBtn" 3 + class="btn text-sm disabled:opacity-50 disabled:cursor-not-allowed" 4 + 5 + {{ if .IsStarred }} 6 + hx-delete="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}" 7 + {{ else }} 8 + hx-post="/star?subject={{.RepoAt}}&countHint={{.Stats.StarCount}}" 9 + {{ end }} 10 + 11 + hx-trigger="click" 12 + hx-target="#starBtn" 13 + hx-swap="outerHTML" 14 + hx-disabled-elt="#starBtn" 15 + > 16 + <div class="flex gap-2 items-center"> 17 + {{ if .IsStarred }} 18 + <span class="w-3 h-3 fill-current" data-lucide="star"></span> 19 + {{ else }} 20 + <span class="w-3 h-3" data-lucide="star"></span> 21 + {{ end }} 22 + <span> 23 + {{ .Stats.StarCount }} 24 + </span> 25 + <span id="starSpinner" class="hidden"> 26 + loading 27 + </span> 28 + </div> 29 + </button> 30 + <script> 31 + document.body.addEventListener('htmx:afterRequest', function (evt) { 32 + lucide.createIcons(); 33 + }); 34 + </script> 35 + {{ end }} 36 +
+19 -11
appview/pages/templates/layouts/repobase.html
··· 2 2 3 3 {{ define "content" }} 4 4 <section id="repo-header" class="mb-4 py-2 px-6"> 5 + <div class="flex gap-3"> 5 6 <p class="text-lg"> 6 - <a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a> 7 - <span class="select-none">/</span> 8 - <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 7 + <a href="/{{ .RepoInfo.OwnerWithAt }}">{{ .RepoInfo.OwnerWithAt }}</a> 8 + <span class="select-none">/</span> 9 + <a href="/{{ .RepoInfo.FullName }}" class="font-bold">{{ .RepoInfo.Name }}</a> 9 10 </p> 10 - <span> 11 - {{ if .RepoInfo.Description }} 12 - {{ .RepoInfo.Description }} 13 - {{ else }} 14 - <span class="italic">this repo has no description</span> 15 - {{ end }} 16 - </span> 11 + {{ template "fragments/star" .RepoInfo }} 12 + </div> 13 + <span> 14 + {{ if .RepoInfo.Description }} 15 + {{ .RepoInfo.Description }} 16 + {{ else }} 17 + <span class="italic">this repo has no description</span> 18 + {{ end }} 19 + </span> 17 20 </section> 18 21 <section id="repo-links" class="min-h-screen flex flex-col drop-shadow-sm"> 19 22 <nav class="w-full mx-auto ml-4"> 20 23 <div class="flex z-60"> 21 24 {{ $activeTabStyles := "-mb-px bg-white" }} 22 25 {{ $tabs := .RepoInfo.GetTabs }} 26 + {{ $tabmeta := .RepoInfo.TabMetadata }} 23 27 {{ range $item := $tabs }} 24 28 {{ $key := index $item 0 }} 25 29 {{ $value := index $item 1 }} 30 + {{ $meta := index $tabmeta $key }} 26 31 <a 27 32 href="/{{ $.RepoInfo.FullName }}{{ $value }}" 28 33 class="relative -mr-px group no-underline hover:no-underline" ··· 42 37 {{ end }} 43 38 " 44 39 > 45 - {{ $key }} 40 + {{ $key }} 41 + {{ if not (isNil $meta) }} 42 + <span class="bg-gray-200 rounded py-1/2 px-1 text-sm font-mono">{{ $meta }}</span> 43 + {{ end }} 46 44 </div> 47 45 </a> 48 46 {{ end }}
+31 -19
appview/pages/templates/timeline.html
··· 40 40 {{ range .Timeline }} 41 41 <div class="px-6 py-2 bg-white rounded drop-shadow-sm w-fit"> 42 42 {{ if .Repo }} 43 - {{ $userHandle := index $.DidHandleMap .Repo.Did }} 44 - <div class="flex items-center"> 45 - <p class="text-gray-600"> 46 - <a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle }}</a> 47 - created 48 - <a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a> 49 - <time class="text-gray-700">{{ .Repo.Created | timeFmt }}</time> 50 - </p> 51 - </div> 43 + {{ $userHandle := index $.DidHandleMap .Repo.Did }} 44 + <div class="flex items-center"> 45 + <p class="text-gray-600"> 46 + <a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle }}</a> 47 + created 48 + <a href="/{{ $userHandle }}/{{ .Repo.Name }}" class="no-underline hover:underline">{{ .Repo.Name }}</a> 49 + <time class="text-gray-700">{{ .Repo.Created | timeFmt }}</time> 50 + </p> 51 + </div> 52 52 {{ else if .Follow }} 53 - {{ $userHandle := index $.DidHandleMap .Follow.UserDid }} 54 - {{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }} 55 - <div class="flex items-center"> 56 - <p class="text-gray-600"> 57 - <a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle }}</a> 58 - followed 59 - <a href="/{{ $subjectHandle }}" class="no-underline hover:underline">{{ $subjectHandle }}</a> 60 - <time class="text-gray-700">{{ .Follow.FollowedAt | timeFmt }}</time> 61 - </p> 62 - </div> 53 + {{ $userHandle := index $.DidHandleMap .Follow.UserDid }} 54 + {{ $subjectHandle := index $.DidHandleMap .Follow.SubjectDid }} 55 + <div class="flex items-center"> 56 + <p class="text-gray-600"> 57 + <a href="/{{ $userHandle }}" class="no-underline hover:underline">{{ $userHandle }}</a> 58 + followed 59 + <a href="/{{ $subjectHandle }}" class="no-underline hover:underline">{{ $subjectHandle }}</a> 60 + <time class="text-gray-700">{{ .Follow.FollowedAt | timeFmt }}</time> 61 + </p> 62 + </div> 63 + {{ else if .Star }} 64 + {{ $starrerHandle := index $.DidHandleMap .Star.StarredByDid }} 65 + {{ $repoOwnerHandle := index $.DidHandleMap .Star.Repo.Did }} 66 + <div class="flex items-center"> 67 + <p class="text-gray-600"> 68 + <a href="/{{ $starrerHandle }}" class="no-underline hover:underline">{{ $starrerHandle }}</a> 69 + starred 70 + <a href="/{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}" class="no-underline hover:underline">{{ $repoOwnerHandle }}/{{ .Star.Repo.Name }}</a> 71 + <time class="text-gray-700">{{ .Star.Created | timeFmt }}</time> 72 + </p> 73 + </div> 63 74 {{ end }} 64 75 </div> 65 76 {{ end }} 66 77 </div> 67 78 </div> 68 79 {{ end }} 80 +
+2 -14
appview/pages/templates/user/profile.html
··· 30 30 </div> 31 31 32 32 {{ if ne .FollowStatus.String "IsSelf" }} 33 - <button id="followBtn" 34 - class="btn mt-2 w-full" 35 - {{ if eq .FollowStatus.String "IsNotFollowing" }} 36 - hx-post="/follow?subject={{.UserDid}}" 37 - {{ else }} 38 - hx-delete="/follow?subject={{.UserDid}}" 39 - {{ end }} 40 - hx-trigger="click" 41 - hx-target="#followBtn" 42 - hx-swap="outerHTML" 43 - > 44 - {{ if eq .FollowStatus.String "IsNotFollowing" }}Follow{{ else }}Unfollow{{ end }} 45 - </button> 46 - {{ end }} 33 + {{ template "fragments/follow" . }} 34 + {{ end }} 47 35 </div> 48 36 {{ end }} 49 37
+11 -22
appview/state/follow.go
··· 1 1 package state 2 2 3 3 import ( 4 - "fmt" 5 4 "log" 6 5 "net/http" 7 6 "time" ··· 9 10 lexutil "github.com/bluesky-social/indigo/lex/util" 10 11 tangled "github.com/sotangled/tangled/api/tangled" 11 12 "github.com/sotangled/tangled/appview/db" 13 + "github.com/sotangled/tangled/appview/pages" 12 14 ) 13 15 14 16 func (s *State) Follow(w http.ResponseWriter, r *http.Request) { ··· 60 60 61 61 log.Println("created atproto record: ", resp.Uri) 62 62 63 - w.Write([]byte(fmt.Sprintf(` 64 - <button id="followBtn" 65 - class="btn mt-2 w-full" 66 - hx-delete="/follow?subject=%s" 67 - hx-trigger="click" 68 - hx-target="#followBtn" 69 - hx-swap="outerHTML"> 70 - Unfollow 71 - </button> 72 - `, subjectIdent.DID.String()))) 63 + s.pages.FollowFragment(w, pages.FollowFragmentParams{ 64 + UserDid: subjectIdent.DID.String(), 65 + FollowStatus: db.IsFollowing, 66 + }) 73 67 74 68 return 75 69 case http.MethodDelete: ··· 77 83 _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 78 84 Collection: tangled.GraphFollowNSID, 79 85 Repo: currentUser.Did, 80 - Rkey: follow.RKey, 86 + Rkey: follow.Rkey, 81 87 }) 82 88 83 89 if err != nil { ··· 91 97 // this is not an issue, the firehose event might have already done this 92 98 } 93 99 94 - w.Write([]byte(fmt.Sprintf(` 95 - <button id="followBtn" 96 - class="btn mt-2 w-full" 97 - hx-post="/follow?subject=%s" 98 - hx-trigger="click" 99 - hx-target="#followBtn" 100 - hx-swap="outerHTML"> 101 - Follow 102 - </button> 103 - `, subjectIdent.DID.String()))) 100 + s.pages.FollowFragment(w, pages.FollowFragmentParams{ 101 + UserDid: subjectIdent.DID.String(), 102 + FollowStatus: db.IsNotFollowing, 103 + }) 104 + 104 105 return 105 106 } 106 107
+20
appview/state/jetstream.go
··· 6 6 "fmt" 7 7 "log" 8 8 9 + "github.com/bluesky-social/indigo/atproto/syntax" 9 10 "github.com/bluesky-social/jetstream/pkg/models" 10 11 tangled "github.com/sotangled/tangled/api/tangled" 11 12 "github.com/sotangled/tangled/appview/db" ··· 41 40 return err 42 41 } 43 42 err = db.AddFollow(d, did, record.Subject, e.Commit.RKey) 43 + if err != nil { 44 + return fmt.Errorf("failed to add follow to db: %w", err) 45 + } 46 + case tangled.FeedStarNSID: 47 + record := tangled.FeedStar{} 48 + err := json.Unmarshal(raw, &record) 49 + if err != nil { 50 + log.Println("invalid record") 51 + return err 52 + } 53 + 54 + subjectUri, err := syntax.ParseATURI(record.Subject) 55 + 56 + if err != nil { 57 + log.Println("invalid record") 58 + return err 59 + } 60 + 61 + err = db.AddStar(d, did, subjectUri, e.Commit.RKey) 44 62 if err != nil { 45 63 return fmt.Errorf("failed to add follow to db: %w", err) 46 64 }
+73 -95
appview/state/repo.go
··· 15 15 "time" 16 16 17 17 "github.com/bluesky-social/indigo/atproto/identity" 18 + "github.com/bluesky-social/indigo/atproto/syntax" 18 19 securejoin "github.com/cyphar/filepath-securejoin" 19 20 "github.com/go-chi/chi/v5" 20 21 "github.com/sotangled/tangled/api/tangled" ··· 76 75 77 76 user := s.auth.GetUser(r) 78 77 79 - knot := f.Knot 80 - if knot == "knot1.tangled.sh" { 81 - knot = "tangled.sh" 82 - } 83 - 84 78 s.pages.RepoIndexPage(w, pages.RepoIndexParams{ 85 - LoggedInUser: user, 86 - RepoInfo: pages.RepoInfo{ 87 - OwnerDid: f.OwnerDid(), 88 - OwnerHandle: f.OwnerHandle(), 89 - Name: f.RepoName, 90 - Knot: knot, 91 - SettingsAllowed: settingsAllowed(s, user, f), 92 - }, 79 + LoggedInUser: user, 80 + RepoInfo: f.RepoInfo(s, user), 93 81 TagMap: tagMap, 94 82 RepoIndexResponse: result, 95 83 }) ··· 123 133 124 134 user := s.auth.GetUser(r) 125 135 s.pages.RepoLog(w, pages.RepoLogParams{ 126 - LoggedInUser: user, 127 - RepoInfo: pages.RepoInfo{ 128 - OwnerDid: f.OwnerDid(), 129 - OwnerHandle: f.OwnerHandle(), 130 - Name: f.RepoName, 131 - SettingsAllowed: settingsAllowed(s, user, f), 132 - }, 136 + LoggedInUser: user, 137 + RepoInfo: f.RepoInfo(s, user), 133 138 RepoLogResponse: repolog, 134 139 }) 135 140 return ··· 159 174 160 175 user := s.auth.GetUser(r) 161 176 s.pages.RepoCommit(w, pages.RepoCommitParams{ 162 - LoggedInUser: user, 163 - RepoInfo: pages.RepoInfo{ 164 - OwnerDid: f.OwnerDid(), 165 - OwnerHandle: f.OwnerHandle(), 166 - Name: f.RepoName, 167 - SettingsAllowed: settingsAllowed(s, user, f), 168 - }, 177 + LoggedInUser: user, 178 + RepoInfo: f.RepoInfo(s, user), 169 179 RepoCommitResponse: result, 170 180 }) 171 181 return ··· 208 228 baseBlobLink := path.Join(f.OwnerDid(), f.RepoName, "blob", ref, treePath) 209 229 210 230 s.pages.RepoTree(w, pages.RepoTreeParams{ 211 - LoggedInUser: user, 212 - BreadCrumbs: breadcrumbs, 213 - BaseTreeLink: baseTreeLink, 214 - BaseBlobLink: baseBlobLink, 215 - RepoInfo: pages.RepoInfo{ 216 - OwnerDid: f.OwnerDid(), 217 - OwnerHandle: f.OwnerHandle(), 218 - Name: f.RepoName, 219 - SettingsAllowed: settingsAllowed(s, user, f), 220 - }, 231 + LoggedInUser: user, 232 + BreadCrumbs: breadcrumbs, 233 + BaseTreeLink: baseTreeLink, 234 + BaseBlobLink: baseBlobLink, 235 + RepoInfo: f.RepoInfo(s, user), 221 236 RepoTreeResponse: result, 222 237 }) 223 238 return ··· 246 271 247 272 user := s.auth.GetUser(r) 248 273 s.pages.RepoTags(w, pages.RepoTagsParams{ 249 - LoggedInUser: user, 250 - RepoInfo: pages.RepoInfo{ 251 - OwnerDid: f.OwnerDid(), 252 - OwnerHandle: f.OwnerHandle(), 253 - Name: f.RepoName, 254 - SettingsAllowed: settingsAllowed(s, user, f), 255 - }, 274 + LoggedInUser: user, 275 + RepoInfo: f.RepoInfo(s, user), 256 276 RepoTagsResponse: result, 257 277 }) 258 278 return ··· 281 311 282 312 user := s.auth.GetUser(r) 283 313 s.pages.RepoBranches(w, pages.RepoBranchesParams{ 284 - LoggedInUser: user, 285 - RepoInfo: pages.RepoInfo{ 286 - OwnerDid: f.OwnerDid(), 287 - OwnerHandle: f.OwnerHandle(), 288 - Name: f.RepoName, 289 - SettingsAllowed: settingsAllowed(s, user, f), 290 - }, 314 + LoggedInUser: user, 315 + RepoInfo: f.RepoInfo(s, user), 291 316 RepoBranchesResponse: result, 292 317 }) 293 318 return ··· 326 361 327 362 user := s.auth.GetUser(r) 328 363 s.pages.RepoBlob(w, pages.RepoBlobParams{ 329 - LoggedInUser: user, 330 - RepoInfo: pages.RepoInfo{ 331 - OwnerDid: f.OwnerDid(), 332 - OwnerHandle: f.OwnerHandle(), 333 - Name: f.RepoName, 334 - SettingsAllowed: settingsAllowed(s, user, f), 335 - }, 364 + LoggedInUser: user, 365 + RepoInfo: f.RepoInfo(s, user), 336 366 RepoBlobResponse: result, 337 367 BreadCrumbs: breadcrumbs, 338 368 }) ··· 448 488 } 449 489 450 490 s.pages.RepoSettings(w, pages.RepoSettingsParams{ 451 - LoggedInUser: user, 452 - RepoInfo: pages.RepoInfo{ 453 - OwnerDid: f.OwnerDid(), 454 - OwnerHandle: f.OwnerHandle(), 455 - Name: f.RepoName, 456 - SettingsAllowed: settingsAllowed(s, user, f), 457 - }, 491 + LoggedInUser: user, 492 + RepoInfo: f.RepoInfo(s, user), 458 493 Collaborators: repoCollaborators, 459 494 IsCollaboratorInviteAllowed: isCollaboratorInviteAllowed, 460 495 }) ··· 460 505 Knot string 461 506 OwnerId identity.Identity 462 507 RepoName string 463 - RepoAt string 508 + RepoAt syntax.ATURI 464 509 } 465 510 466 511 func (f *FullyResolvedRepo) OwnerDid() string { ··· 520 565 return collaborators, nil 521 566 } 522 567 568 + func (f *FullyResolvedRepo) RepoInfo(s *State, u *auth.User) pages.RepoInfo { 569 + isStarred := false 570 + if u != nil { 571 + isStarred = db.GetStarStatus(s.db, u.Did, syntax.ATURI(f.RepoAt)) 572 + } 573 + 574 + starCount, err := db.GetStarCount(s.db, f.RepoAt) 575 + if err != nil { 576 + log.Println("failed to get star count for ", f.RepoAt) 577 + } 578 + issueCount, err := db.GetIssueCount(s.db, f.RepoAt) 579 + if err != nil { 580 + log.Println("failed to get issue count for ", f.RepoAt) 581 + } 582 + 583 + knot := f.Knot 584 + if knot == "knot1.tangled.sh" { 585 + knot = "tangled.sh" 586 + } 587 + 588 + return pages.RepoInfo{ 589 + OwnerDid: f.OwnerDid(), 590 + OwnerHandle: f.OwnerHandle(), 591 + Name: f.RepoName, 592 + RepoAt: f.RepoAt, 593 + SettingsAllowed: settingsAllowed(s, u, f), 594 + IsStarred: isStarred, 595 + Knot: knot, 596 + Stats: db.RepoStats{ 597 + StarCount: starCount, 598 + IssueCount: issueCount, 599 + }, 600 + } 601 + } 602 + 523 603 func (s *State) RepoSingleIssue(w http.ResponseWriter, r *http.Request) { 524 604 user := s.auth.GetUser(r) 525 605 f, err := fullyResolvedRepo(r) ··· 599 609 600 610 s.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{ 601 611 LoggedInUser: user, 602 - RepoInfo: pages.RepoInfo{ 603 - OwnerDid: f.OwnerDid(), 604 - OwnerHandle: f.OwnerHandle(), 605 - Name: f.RepoName, 606 - SettingsAllowed: settingsAllowed(s, user, f), 607 - }, 608 - Issue: *issue, 609 - Comments: comments, 612 + RepoInfo: f.RepoInfo(s, user), 613 + Issue: *issue, 614 + Comments: comments, 610 615 611 616 IssueOwnerHandle: issueOwnerIdent.Handle.String(), 612 617 DidHandleMap: didHandleMap, ··· 762 777 return 763 778 } 764 779 780 + atUri := f.RepoAt.String() 765 781 client, _ := s.auth.AuthorizedClient(r) 766 782 _, err = comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 767 783 Collection: tangled.RepoIssueCommentNSID, ··· 770 784 Rkey: s.TID(), 771 785 Record: &lexutil.LexiconTypeDecoder{ 772 786 Val: &tangled.RepoIssueComment{ 773 - Repo: &f.RepoAt, 787 + Repo: &atUri, 774 788 Issue: issueAt, 775 789 CommentId: &commentIdInt64, 776 790 Owner: &ownerDid, ··· 821 835 822 836 s.pages.RepoIssues(w, pages.RepoIssuesParams{ 823 837 LoggedInUser: s.auth.GetUser(r), 824 - RepoInfo: pages.RepoInfo{ 825 - OwnerDid: f.OwnerDid(), 826 - OwnerHandle: f.OwnerHandle(), 827 - Name: f.RepoName, 828 - SettingsAllowed: settingsAllowed(s, user, f), 829 - }, 838 + RepoInfo: f.RepoInfo(s, user), 830 839 Issues: issues, 831 840 DidHandleMap: didHandleMap, 832 841 }) ··· 841 860 case http.MethodGet: 842 861 s.pages.RepoNewIssue(w, pages.RepoNewIssueParams{ 843 862 LoggedInUser: user, 844 - RepoInfo: pages.RepoInfo{ 845 - Name: f.RepoName, 846 - OwnerDid: f.OwnerDid(), 847 - OwnerHandle: f.OwnerHandle(), 848 - SettingsAllowed: settingsAllowed(s, user, f), 849 - }, 863 + RepoInfo: f.RepoInfo(s, user), 850 864 }) 851 865 case http.MethodPost: 852 866 title := r.FormValue("title") ··· 878 902 } 879 903 880 904 client, _ := s.auth.AuthorizedClient(r) 905 + atUri := f.RepoAt.String() 881 906 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 882 907 Collection: tangled.RepoIssueNSID, 883 908 Repo: user.Did, 884 909 Rkey: s.TID(), 885 910 Record: &lexutil.LexiconTypeDecoder{ 886 911 Val: &tangled.RepoIssue{ 887 - Repo: f.RepoAt, 912 + Repo: atUri, 888 913 Title: title, 889 914 Body: &body, 890 915 Owner: user.Did, ··· 923 946 case http.MethodGet: 924 947 s.pages.RepoPulls(w, pages.RepoPullsParams{ 925 948 LoggedInUser: user, 926 - RepoInfo: pages.RepoInfo{ 927 - Name: f.RepoName, 928 - OwnerDid: f.OwnerDid(), 929 - OwnerHandle: f.OwnerHandle(), 930 - SettingsAllowed: settingsAllowed(s, user, f), 931 - }, 949 + RepoInfo: f.RepoInfo(s, user), 932 950 }) 933 951 } 934 952 } ··· 947 975 return nil, fmt.Errorf("malformed middleware") 948 976 } 949 977 978 + parsedRepoAt, err := syntax.ParseATURI(repoAt) 979 + if err != nil { 980 + log.Println("malformed repo at-uri") 981 + return nil, fmt.Errorf("malformed middleware") 982 + } 983 + 950 984 return &FullyResolvedRepo{ 951 985 Knot: knot, 952 986 OwnerId: id, 953 987 RepoName: repoName, 954 - RepoAt: repoAt, 988 + RepoAt: parsedRepoAt, 955 989 }, nil 956 990 } 957 991
+115
appview/state/star.go
··· 1 + package state 2 + 3 + import ( 4 + "log" 5 + "net/http" 6 + "time" 7 + 8 + comatproto "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/syntax" 10 + lexutil "github.com/bluesky-social/indigo/lex/util" 11 + tangled "github.com/sotangled/tangled/api/tangled" 12 + "github.com/sotangled/tangled/appview/db" 13 + "github.com/sotangled/tangled/appview/pages" 14 + ) 15 + 16 + func (s *State) Star(w http.ResponseWriter, r *http.Request) { 17 + currentUser := s.auth.GetUser(r) 18 + 19 + subject := r.URL.Query().Get("subject") 20 + if subject == "" { 21 + log.Println("invalid form") 22 + return 23 + } 24 + 25 + subjectUri, err := syntax.ParseATURI(subject) 26 + if err != nil { 27 + log.Println("invalid form") 28 + return 29 + } 30 + 31 + client, _ := s.auth.AuthorizedClient(r) 32 + 33 + switch r.Method { 34 + case http.MethodPost: 35 + createdAt := time.Now().Format(time.RFC3339) 36 + rkey := s.TID() 37 + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ 38 + Collection: tangled.FeedStarNSID, 39 + Repo: currentUser.Did, 40 + Rkey: rkey, 41 + Record: &lexutil.LexiconTypeDecoder{ 42 + Val: &tangled.FeedStar{ 43 + Subject: subjectUri.String(), 44 + CreatedAt: createdAt, 45 + }}, 46 + }) 47 + if err != nil { 48 + log.Println("failed to create atproto record", err) 49 + return 50 + } 51 + 52 + err = db.AddStar(s.db, currentUser.Did, subjectUri, rkey) 53 + if err != nil { 54 + log.Println("failed to star", err) 55 + return 56 + } 57 + 58 + starCount, err := db.GetStarCount(s.db, subjectUri) 59 + if err != nil { 60 + log.Println("failed to get star count for ", subjectUri) 61 + } 62 + 63 + log.Println("created atproto record: ", resp.Uri) 64 + 65 + s.pages.StarFragment(w, pages.StarFragmentParams{ 66 + IsStarred: true, 67 + RepoAt: subjectUri, 68 + Stats: db.RepoStats{ 69 + StarCount: starCount, 70 + }, 71 + }) 72 + 73 + return 74 + case http.MethodDelete: 75 + // find the record in the db 76 + star, err := db.GetStar(s.db, currentUser.Did, subjectUri) 77 + if err != nil { 78 + log.Println("failed to get star relationship") 79 + return 80 + } 81 + 82 + _, err = comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{ 83 + Collection: tangled.FeedStarNSID, 84 + Repo: currentUser.Did, 85 + Rkey: star.Rkey, 86 + }) 87 + 88 + if err != nil { 89 + log.Println("failed to unstar") 90 + return 91 + } 92 + 93 + err = db.DeleteStar(s.db, currentUser.Did, subjectUri) 94 + if err != nil { 95 + log.Println("failed to delete star from DB") 96 + // this is not an issue, the firehose event might have already done this 97 + } 98 + 99 + starCount, err := db.GetStarCount(s.db, subjectUri) 100 + if err != nil { 101 + log.Println("failed to get star count for ", subjectUri) 102 + } 103 + 104 + s.pages.StarFragment(w, pages.StarFragmentParams{ 105 + IsStarred: false, 106 + RepoAt: subjectUri, 107 + Stats: db.RepoStats{ 108 + StarCount: starCount, 109 + }, 110 + }) 111 + 112 + return 113 + } 114 + 115 + }
+9 -2
appview/state/state.go
··· 184 184 didsToResolve = append(didsToResolve, ev.Repo.Did) 185 185 } 186 186 if ev.Follow != nil { 187 - didsToResolve = append(didsToResolve, ev.Follow.UserDid) 188 - didsToResolve = append(didsToResolve, ev.Follow.SubjectDid) 187 + didsToResolve = append(didsToResolve, ev.Follow.UserDid, ev.Follow.SubjectDid) 188 + } 189 + if ev.Star != nil { 190 + didsToResolve = append(didsToResolve, ev.Star.StarredByDid, ev.Star.Repo.Did) 189 191 } 190 192 } 191 193 ··· 933 931 r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) { 934 932 r.Post("/", s.Follow) 935 933 r.Delete("/", s.Follow) 934 + }) 935 + 936 + r.With(AuthMiddleware(s)).Route("/star", func(r chi.Router) { 937 + r.Post("/", s.Star) 938 + r.Delete("/", s.Star) 936 939 }) 937 940 938 941 r.Route("/settings", func(r chi.Router) {
+6 -5
cmd/gen.go
··· 14 14 if err := genCfg.WriteMapEncodersToFile( 15 15 "api/tangled/cbor_gen.go", 16 16 "tangled", 17 - shtangled.PublicKey{}, 18 - shtangled.KnotMember{}, 17 + shtangled.FeedStar{}, 19 18 shtangled.GraphFollow{}, 20 - shtangled.Repo{}, 21 - shtangled.RepoIssue{}, 22 - shtangled.RepoIssueState{}, 19 + shtangled.KnotMember{}, 20 + shtangled.PublicKey{}, 23 21 shtangled.RepoIssueComment{}, 22 + shtangled.RepoIssueState{}, 23 + shtangled.RepoIssue{}, 24 + shtangled.Repo{}, 24 25 ); err != nil { 25 26 panic(err) 26 27 }
+31
lexicons/star.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.tangled.feed.star", 4 + "needsCbor": true, 5 + "needsType": true, 6 + "defs": { 7 + "main": { 8 + "type": "record", 9 + "key": "tid", 10 + "record": { 11 + "type": "object", 12 + "required": [ 13 + "createdAt", 14 + "subject" 15 + ], 16 + "properties": { 17 + "createdAt": { 18 + "type": "string", 19 + "format": "datetime" 20 + }, 21 + "subject": { 22 + "type": "string", 23 + "format": "at-uri" 24 + } 25 + } 26 + } 27 + } 28 + } 29 + } 30 + 31 +