Monorepo for Tangled tangled.org

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

merged opened by willdot.net targeting master from willdot.net/tangled-fork: punchcard-prefs

This allows users to set their preference for the punchcard.

It allows them to either:

1: Hide their punchcard so that no one can see it 2: Hide all punchcards so that they don't see anyones punchcards

Labels

None yet.

assignee

None yet.

Participants 2
AT URI
at://did:plc:dadhhalkfcq3gucaq25hjqon/sh.tangled.repo.pull/3meygakiwru22
+190 -25
Diff #4
+7
appview/db/db.go
··· 573 573 name text unique 574 574 ); 575 575 576 + create table if not exists punchcard_preferences ( 577 + id integer primary key autoincrement, 578 + user_did text not null unique, 579 + hide_mine integer default 0, 580 + hide_others integer default 0 581 + ); 582 + 576 583 -- indexes for better performance 577 584 create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); 578 585 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 16 17 17 const TimeframeMonths = 7 18 18 19 - func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, error) { 19 + func MakeProfileTimeline(e Execer, forDid string, includePunchcard bool) (*models.ProfileTimeline, error) { 20 20 timeline := models.ProfileTimeline{ 21 21 ByMonth: make([]models.ByMonth, TimeframeMonths), 22 22 } ··· 98 98 }) 99 99 } 100 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 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) 112 109 } 110 + for _, punch := range punchcard.Punches { 111 + if punch.Date.After(now) { 112 + continue 113 + } 113 114 114 - monthsAgo := monthsBetween(punch.Date, now) 115 - if monthsAgo >= TimeframeMonths { 116 - // shouldn't happen; but times are weird 117 - continue 118 - } 115 + monthsAgo := monthsBetween(punch.Date, now) 116 + if monthsAgo >= TimeframeMonths { 117 + // shouldn't happen; but times are weird 118 + continue 119 + } 119 120 120 - idx := monthsAgo 121 - timeline.ByMonth[idx].Commits += punch.Count 121 + idx := monthsAgo 122 + timeline.ByMonth[idx].Commits += punch.Count 123 + } 122 124 } 123 125 124 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 359 } 360 360 361 361 type UserProfileSettingsParams struct { 362 - LoggedInUser *oauth.MultiAccountUser 363 - Tab string 362 + LoggedInUser *oauth.MultiAccountUser 363 + Tab string 364 + PunchcardPreference models.PunchcardPreference 364 365 } 365 366 366 367 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { ··· 557 558 ProfileTimeline *models.ProfileTimeline 558 559 Card *ProfileCard 559 560 Active string 561 + ShowPunchcard bool 560 562 } 561 563 562 564 func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
+3 -1
appview/pages/templates/layouts/profilebase.html
··· 52 52 <div class="{{ $style }} order-1 order-1"> 53 53 <div class="flex flex-col gap-4"> 54 54 {{ template "user/fragments/profileCard" .Card }} 55 - {{ block "punchcard" .Card.Punchcard }} {{ end }} 55 + {{ if .ShowPunchcard }} 56 + {{ block "punchcard" .Card.Punchcard }} {{ end }} 57 + {{ end }} 56 58 </div> 57 59 </div> 58 60
+23
appview/pages/templates/user/settings/profile.html
··· 59 59 </div> 60 60 </div> 61 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> 62 85 {{ end }}
+7 -1
appview/settings/settings.go
··· 70 70 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 71 71 user := s.OAuth.GetMultiAccountUser(r) 72 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 + 73 78 s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 74 - LoggedInUser: user, 79 + LoggedInUser: user, 80 + PunchcardPreference: punchcardPreferences, 75 81 }) 76 82 } 77 83
+64 -2
appview/state/profile.go
··· 4 4 "context" 5 5 "fmt" 6 6 "log" 7 + "log/slog" 7 8 "net/http" 8 9 "slices" 9 10 "strings" ··· 164 165 } 165 166 } 166 167 167 - timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 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) 168 173 if err != nil { 169 174 l.Error("failed to create timeline", "err", err) 170 175 } ··· 175 180 Repos: pinnedRepos, 176 181 CollaboratingRepos: pinnedCollaboratingRepos, 177 182 ProfileTimeline: timeline, 183 + ShowPunchcard: showPunchcard, 178 184 }) 179 185 } 180 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 + 181 216 func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { 182 217 l := s.logger.With("handler", "reposPage") 183 218 ··· 411 446 } 412 447 413 448 func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 414 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 449 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String(), false) 415 450 if err != nil { 416 451 return nil, err 417 452 } ··· 936 971 937 972 s.pages.HxRedirect(w, r.Header.Get("Referer")) 938 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 167 r.Post("/pins", s.UpdateProfilePins) 168 168 r.Post("/avatar", s.UploadProfileAvatar) 169 169 r.Delete("/avatar", s.RemoveProfileAvatar) 170 + r.Post("/punchcard", s.UpdateProfilePunchcardSetting) 170 171 }) 171 172 172 173 r.Mount("/settings", s.SettingsRouter())

History

5 rounds 3 comments
sign up or login to add to the discussion
1 commit
expand
appview: allow users to set their preferences for the punchcard being displayed
expand 1 comment

thanks for the work on this! this looks good to me in the current state.

pull request successfully merged
1 commit
expand
appview: allow users to set their preferences for the punchcard being displayed
expand 0 comments
1 commit
expand
appview: allow users to set their preferences for the punchcard being displayed
expand 0 comments
1 commit
expand
appview: allow users to set their preferences for the punchcard being displayed
expand 0 comments
1 commit
expand
appview: allow users to set their preferences for the punchcard being displayed
expand 2 comments
  • here: there is no bool type in sqlite! so we will have to use ints here. i tested this locally and it seems that is valid sql and executes fine, but it seems any value can be stored in that column.
  • here: nice, this is super neat

all in all, rest of the code lgtm!

Oh wow, TIL that there isn't a bool type in sqlite ๐Ÿซฃ

I've fixed that so that the field is an integer type with a default of 0 (false) and then added the relevant mapping into the query code to map 0 is false and > 0 is true.

Tested by stuffing random integers manually into the table and appears to work as expected.