Monorepo for Tangled

appview: allow users to set their preferences for the punchcard being displayed

Signed-off-by: Will Andrews <will7989@hotmail.com>

authored by willdot.net and committed by tangled.org 2482a23f 0dee98c3

+190 -25
+7
appview/db/db.go
··· 601 name text unique 602 ); 603 604 -- indexes for better performance 605 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 606 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
··· 601 name text unique 602 ); 603 604 + create table if not exists punchcard_preferences ( 605 + id integer primary key autoincrement, 606 + user_did text not null unique, 607 + hide_mine integer default 0, 608 + hide_others integer default 0 609 + ); 610 + 611 -- indexes for better performance 612 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 613 create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read);
+52
appview/db/preferences.go
···
··· 1 + package db 2 + 3 + import ( 4 + "database/sql" 5 + 6 + "tangled.org/core/appview/models" 7 + ) 8 + 9 + func GetPunchcardPreference(e Execer, did string) (models.PunchcardPreference, error) { 10 + preference := models.PunchcardPreference{ 11 + Did: did, 12 + } 13 + 14 + hideMine := 0 15 + hideOthers := 0 16 + 17 + err := e.QueryRow( 18 + `select id, hide_mine, hide_others from punchcard_preferences where user_did = ?`, 19 + did, 20 + ).Scan(&preference.ID, &hideMine, &hideOthers) 21 + if err == sql.ErrNoRows { 22 + return preference, nil 23 + } 24 + 25 + preference.HideMine = hideMine > 0 26 + preference.HideOthers = hideOthers > 0 27 + 28 + if err != nil { 29 + return preference, err 30 + } 31 + 32 + return preference, nil 33 + } 34 + 35 + func UpsertPunchcardPreference(e Execer, did string, hideMine, hideOthers bool) error { 36 + _, err := e.Exec( 37 + `insert or replace into punchcard_preferences ( 38 + user_did, 39 + hide_mine, 40 + hide_others 41 + ) 42 + values (?, ?, ?)`, 43 + did, 44 + hideMine, 45 + hideOthers, 46 + ) 47 + if err != nil { 48 + return err 49 + } 50 + 51 + return nil 52 + }
+21 -19
appview/db/profile.go
··· 16 17 const TimeframeMonths = 7 18 19 - func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, error) { 20 timeline := models.ProfileTimeline{ 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 } ··· 98 }) 99 } 100 101 - punchcard, err := MakePunchcard( 102 - e, 103 - orm.FilterEq("did", forDid), 104 - orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), 105 - ) 106 - if err != nil { 107 - return nil, fmt.Errorf("error getting commits by did: %w", err) 108 - } 109 - for _, punch := range punchcard.Punches { 110 - if punch.Date.After(now) { 111 - continue 112 } 113 114 - monthsAgo := monthsBetween(punch.Date, now) 115 - if monthsAgo >= TimeframeMonths { 116 - // shouldn't happen; but times are weird 117 - continue 118 } 119 - 120 - idx := monthsAgo 121 - timeline.ByMonth[idx].Commits += punch.Count 122 } 123 124 return &timeline, nil
··· 16 17 const TimeframeMonths = 7 18 19 + func MakeProfileTimeline(e Execer, forDid string, includePunchcard bool) (*models.ProfileTimeline, error) { 20 timeline := models.ProfileTimeline{ 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 } ··· 98 }) 99 } 100 101 + if includePunchcard { 102 + punchcard, err := MakePunchcard( 103 + e, 104 + orm.FilterEq("did", forDid), 105 + orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), 106 + ) 107 + if err != nil { 108 + return nil, fmt.Errorf("error getting commits by did: %w", err) 109 } 110 + for _, punch := range punchcard.Punches { 111 + if punch.Date.After(now) { 112 + continue 113 + } 114 + 115 + monthsAgo := monthsBetween(punch.Date, now) 116 + if monthsAgo >= TimeframeMonths { 117 + // shouldn't happen; but times are weird 118 + continue 119 + } 120 121 + idx := monthsAgo 122 + timeline.ByMonth[idx].Commits += punch.Count 123 } 124 } 125 126 return &timeline, nil
+8
appview/models/preferences.go
···
··· 1 + package models 2 + 3 + type PunchcardPreference struct { 4 + ID int 5 + Did string 6 + HideMine bool 7 + HideOthers bool 8 + }
+4 -2
appview/pages/pages.go
··· 359 } 360 361 type UserProfileSettingsParams struct { 362 - LoggedInUser *oauth.MultiAccountUser 363 - Tab string 364 } 365 366 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { ··· 557 ProfileTimeline *models.ProfileTimeline 558 Card *ProfileCard 559 Active string 560 } 561 562 func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
··· 359 } 360 361 type UserProfileSettingsParams struct { 362 + LoggedInUser *oauth.MultiAccountUser 363 + Tab string 364 + PunchcardPreference models.PunchcardPreference 365 } 366 367 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { ··· 558 ProfileTimeline *models.ProfileTimeline 559 Card *ProfileCard 560 Active string 561 + ShowPunchcard bool 562 } 563 564 func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
+3 -1
appview/pages/templates/layouts/profilebase.html
··· 52 <div class="{{ $style }} order-1 order-1"> 53 <div class="flex flex-col gap-4"> 54 {{ template "user/fragments/profileCard" .Card }} 55 - {{ block "punchcard" .Card.Punchcard }} {{ end }} 56 </div> 57 </div> 58
··· 52 <div class="{{ $style }} order-1 order-1"> 53 <div class="flex flex-col gap-4"> 54 {{ template "user/fragments/profileCard" .Card }} 55 + {{ if .ShowPunchcard }} 56 + {{ block "punchcard" .Card.Punchcard }} {{ end }} 57 + {{ end }} 58 </div> 59 </div> 60
+23
appview/pages/templates/user/settings/profile.html
··· 59 </div> 60 </div> 61 </div> 62 {{ end }}
··· 59 </div> 60 </div> 61 </div> 62 + <div class="flex flex-col rounded border border-gray-200 dark:border-gray-700 divide-y divide-gray-200 dark:divide-gray-700 w-full"> 63 + <div class="flex items-center justify-between p-4"> 64 + <div class="hover:no-underline flex flex-col gap-1 min-w-0 max-w-[80%]"> 65 + <div class="flex flex-wrap text-sm items-center gap-1 text-gray-500 dark:text-gray-400"> 66 + <span>Punchcard settings</span> 67 + </div> 68 + <form hx-post="/profile/punchcard" class="col-span-1 md:col-span-1 md:justify-self-end group flex gap-2 items-stretch"> 69 + <div> 70 + <input type="checkbox" id="hideMine" name="hideMine" value="on" {{ if eq true $.PunchcardPreference.HideMine }}checked{{ end }}> 71 + <label for="hideMine" class="my-0 py-0 normal-case font-normal">Hide mine</label> 72 + </div> 73 + <div> 74 + <input type="checkbox" id="hideOthers" name="hideOthers" value="on" {{ if eq true $.PunchcardPreference.HideOthers }}checked{{ end }}> 75 + <label for="hideOthers" class="my-0 py-0 normal-case font-normal">Hide others from me</label> 76 + </div> 77 + <button class="btn flex gap-2 items-center" type="submit"> 78 + {{ i "check" "size-4" }} 79 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 80 + </button> 81 + </form> 82 + </div> 83 + </div> 84 + </div> 85 {{ end }}
+7 -1
appview/settings/settings.go
··· 70 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 71 user := s.OAuth.GetMultiAccountUser(r) 72 73 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 74 - LoggedInUser: user, 75 }) 76 } 77
··· 70 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 71 user := s.OAuth.GetMultiAccountUser(r) 72 73 + punchcardPreferences, err := db.GetPunchcardPreference(s.Db, user.Did()) 74 + if err != nil { 75 + log.Printf("failed to get users punchcard preferences: %s", err) 76 + } 77 + 78 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 79 + LoggedInUser: user, 80 + PunchcardPreference: punchcardPreferences, 81 }) 82 } 83
+64 -2
appview/state/profile.go
··· 4 "context" 5 "fmt" 6 "log" 7 "net/http" 8 "slices" 9 "strings" ··· 164 } 165 } 166 167 - timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 168 if err != nil { 169 l.Error("failed to create timeline", "err", err) 170 } ··· 175 Repos: pinnedRepos, 176 CollaboratingRepos: pinnedCollaboratingRepos, 177 ProfileTimeline: timeline, 178 }) 179 } 180 181 func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { ··· 411 } 412 413 func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 414 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 415 if err != nil { 416 return nil, err 417 } ··· 936 937 s.pages.HxRedirect(w, r.Header.Get("Referer")) 938 }
··· 4 "context" 5 "fmt" 6 "log" 7 + "log/slog" 8 "net/http" 9 "slices" 10 "strings" ··· 165 } 166 } 167 168 + loggedInUser := s.oauth.GetMultiAccountUser(r) 169 + 170 + showPunchcard := checkIfPunchcardShouldShow(s.db, l, profile.UserDid, loggedInUser.Did()) 171 + 172 + timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid, showPunchcard) 173 if err != nil { 174 l.Error("failed to create timeline", "err", err) 175 } ··· 180 Repos: pinnedRepos, 181 CollaboratingRepos: pinnedCollaboratingRepos, 182 ProfileTimeline: timeline, 183 + ShowPunchcard: showPunchcard, 184 }) 185 + } 186 + 187 + func checkIfPunchcardShouldShow(e db.Execer, l *slog.Logger, targetDid, requesterDid string) bool { 188 + targetPunchcardPreferences, err := db.GetPunchcardPreference(e, targetDid) 189 + if err != nil { 190 + l.Error("failed to get target users punchcard preferences", "err", err) 191 + return true 192 + } 193 + 194 + requesterPunchcardPreferences, err := db.GetPunchcardPreference(e, requesterDid) 195 + if err != nil { 196 + l.Error("failed to get requester users punchcard preferences", "err", err) 197 + return true 198 + } 199 + 200 + showPunchcard := true 201 + 202 + // looking at their own profile 203 + if targetDid == requesterDid { 204 + if targetPunchcardPreferences.HideMine { 205 + return false 206 + } 207 + return true 208 + } 209 + 210 + if targetPunchcardPreferences.HideMine || requesterPunchcardPreferences.HideOthers { 211 + showPunchcard = false 212 + } 213 + return showPunchcard 214 } 215 216 func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { ··· 446 } 447 448 func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 449 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String(), false) 450 if err != nil { 451 return nil, err 452 } ··· 971 972 s.pages.HxRedirect(w, r.Header.Get("Referer")) 973 } 974 + 975 + func (s *State) UpdateProfilePunchcardSetting(w http.ResponseWriter, r *http.Request) { 976 + err := r.ParseForm() 977 + if err != nil { 978 + log.Println("invalid profile update form", err) 979 + return 980 + } 981 + user := s.oauth.GetUser(r) 982 + 983 + hideOthers := false 984 + hideMine := false 985 + 986 + if r.Form.Get("hideMine") == "on" { 987 + hideMine = true 988 + } 989 + if r.Form.Get("hideOthers") == "on" { 990 + hideOthers = true 991 + } 992 + 993 + err = db.UpsertPunchcardPreference(s.db, user.Did, hideMine, hideOthers) 994 + if err != nil { 995 + log.Println("failed to update punchcard preferences", err) 996 + return 997 + } 998 + 999 + s.pages.HxRefresh(w) 1000 + }
+1
appview/state/router.go
··· 167 r.Post("/pins", s.UpdateProfilePins) 168 r.Post("/avatar", s.UploadProfileAvatar) 169 r.Delete("/avatar", s.RemoveProfileAvatar) 170 }) 171 172 r.Mount("/settings", s.SettingsRouter())
··· 167 r.Post("/pins", s.UpdateProfilePins) 168 r.Post("/avatar", s.UploadProfileAvatar) 169 r.Delete("/avatar", s.RemoveProfileAvatar) 170 + r.Post("/punchcard", s.UpdateProfilePunchcardSetting) 171 }) 172 173 r.Mount("/settings", s.SettingsRouter())