Monorepo for Tangled tangled.org

appview: allow users to decide how the profile punchcard is displayed #1018

open opened by willdot.net targeting master from willdot.net/tangled-fork: disable-punchcard

This solves https://tangled.org/tangled.org/core/issues/189 although the issue wasn't clear if it was to hide the punchcard for me (the user) so that I don't see any punchcards, or if it hides my punchcard from being seen by other users.

I've implemented both so that you can either disable your punchcard from being seen by any one, or you can hide everyones punchcards from being seen by you.

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:dadhhalkfcq3gucaq25hjqon/sh.tangled.repo.pull/3mdbrb5a7cn22
+177 -39
Diff #0
+8 -1
appview/db/db.go
··· 1078 // transfer data, constructing pull_at from pulls table 1079 _, err = tx.Exec(` 1080 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1081 - select 1082 ps.id, 1083 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1084 ps.round_number, ··· 1173 return err 1174 }) 1175 1176 return &DB{ 1177 db, 1178 logger,
··· 1078 // transfer data, constructing pull_at from pulls table 1079 _, err = tx.Exec(` 1080 insert into pull_submissions_new (id, pull_at, round_number, patch, created) 1081 + select 1082 ps.id, 1083 'at://' || p.owner_did || '/sh.tangled.repo.pull/' || p.rkey, 1084 ps.round_number, ··· 1173 return err 1174 }) 1175 1176 + orm.RunMigration(conn, logger, "add-punchcard-setting-profile", func(tx *sql.Tx) error { 1177 + _, err := tx.Exec(` 1178 + alter table profile add column punchard_setting string; 1179 + `) 1180 + return err 1181 + }) 1182 + 1183 return &DB{ 1184 db, 1185 logger,
+31 -21
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 ··· 353 includeBluesky := 0 354 355 err := e.QueryRow( 356 - `select description, include_bluesky, location, pronouns from profile where did = ?`, 357 did, 358 - ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns) 359 if err == sql.ErrNoRows { 360 profile := models.Profile{} 361 profile.Did = did ··· 536 } 537 return nil 538 }
··· 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 ··· 355 includeBluesky := 0 356 357 err := e.QueryRow( 358 + `select description, include_bluesky, location, pronouns, punchard_setting from profile where did = ?`, 359 did, 360 + ).Scan(&profile.Description, &includeBluesky, &profile.Location, &pronouns, &profile.PunchardSetting) 361 if err == sql.ErrNoRows { 362 profile := models.Profile{} 363 profile.Did = did ··· 538 } 539 return nil 540 } 541 + 542 + func SetProfilePunchcardStatus(e Execer, did string, punchcard models.ProfilePunchcardOption) error { 543 + _, err := e.Exec( 544 + `update profile set punchard_setting = ? where did = ?`, 545 + punchcard, did, 546 + ) 547 + return err 548 + }
+26 -7
appview/models/profile.go
··· 7 "tangled.org/core/api/tangled" 8 ) 9 10 type Profile struct { 11 // ids 12 ID int 13 Did string 14 15 // data 16 - Description string 17 - IncludeBluesky bool 18 - Location string 19 - Links [5]string 20 - Stats [2]VanityStat 21 - PinnedRepos [6]syntax.ATURI 22 - Pronouns string 23 } 24 25 func (p Profile) IsLinksEmpty() bool {
··· 7 "tangled.org/core/api/tangled" 8 ) 9 10 + type ProfilePunchcardOption string 11 + 12 + const ( 13 + ProfilePunchcardOptionHideMine ProfilePunchcardOption = "HIDE_MINE" 14 + ProfilePunchcardOptionHideAll ProfilePunchcardOption = "HIDE_ALL" 15 + ) 16 + 17 + func ProfilePunchcardFromString(s string) ProfilePunchcardOption { 18 + switch s { 19 + case "HIDE_MINE": 20 + return ProfilePunchcardOptionHideMine 21 + case "HIDE_ALL": 22 + return ProfilePunchcardOptionHideAll 23 + default: 24 + return "" 25 + } 26 + } 27 + 28 type Profile struct { 29 // ids 30 ID int 31 Did string 32 33 // data 34 + Description string 35 + IncludeBluesky bool 36 + Location string 37 + Links [5]string 38 + Stats [2]VanityStat 39 + PinnedRepos [6]syntax.ATURI 40 + Pronouns string 41 + PunchardSetting ProfilePunchcardOption 42 } 43 44 func (p Profile) IsLinksEmpty() bool {
+5 -3
appview/pages/pages.go
··· 337 } 338 339 type UserProfileSettingsParams struct { 340 - LoggedInUser *oauth.MultiAccountUser 341 - Tabs []map[string]any 342 - Tab string 343 } 344 345 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { ··· 537 ProfileTimeline *models.ProfileTimeline 538 Card *ProfileCard 539 Active string 540 } 541 542 func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
··· 337 } 338 339 type UserProfileSettingsParams struct { 340 + LoggedInUser *oauth.MultiAccountUser 341 + Tabs []map[string]any 342 + Tab string 343 + PunchcardSetting models.ProfilePunchcardOption 344 } 345 346 func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { ··· 538 ProfileTimeline *models.ProfileTimeline 539 Card *ProfileCard 540 Active string 541 + ShowPunchcard bool 542 } 543 544 func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error {
+4 -2
appview/pages/templates/layouts/profilebase.html
··· 10 <meta property="og:image" content="{{ $avatarUrl }}" /> 11 <meta property="og:image:width" content="512" /> 12 <meta property="og:image:height" content="512" /> 13 - 14 <meta name="twitter:card" content="summary" /> 15 <meta name="twitter:title" content="{{ $handle }}" /> 16 <meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" /> ··· 28 <div class="{{ $style }} order-1 order-1"> 29 <div class="flex flex-col gap-4"> 30 {{ template "user/fragments/profileCard" .Card }} 31 - {{ block "punchcard" .Card.Punchcard }} {{ end }} 32 </div> 33 </div> 34
··· 10 <meta property="og:image" content="{{ $avatarUrl }}" /> 11 <meta property="og:image:width" content="512" /> 12 <meta property="og:image:height" content="512" /> 13 + 14 <meta name="twitter:card" content="summary" /> 15 <meta name="twitter:title" content="{{ $handle }}" /> 16 <meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" /> ··· 28 <div class="{{ $style }} order-1 order-1"> 29 <div class="flex flex-col gap-4"> 30 {{ template "user/fragments/profileCard" .Card }} 31 + {{ if .ShowPunchcard }} 32 + {{ block "punchcard" .Card.Punchcard }} {{ end }} 33 + {{ end }} 34 </div> 35 </div> 36
+31
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 + <select 70 + id="punchcard-setting" 71 + name="punchcard-setting" 72 + required 73 + class="p-1 max-w-64 border border-gray-200 bg-white dark:bg-gray-800 dark:text-white dark:border-gray-700"> 74 + {{/* For some reason, we can't use an empty string in a <select> in all scenarios unless it is preceded by a disabled select?? No idea, could just be a Firefox thing? */}} 75 + <option value="[[none]]" class="py-1" {{ if not $.PunchcardSetting }}selected{{ end }}> 76 + Show all 77 + </option> 78 + <option value="HIDE_MINE" class="py-1" {{ if eq "HIDE_MINE" $.PunchcardSetting }}selected{{ end }}> 79 + Hide mine for others 80 + </option> 81 + <option value="HIDE_ALL" class="py-1" {{ if eq "HIDE_ALL" $.PunchcardSetting }}selected{{ end }}> 82 + Hide everyones for me 83 + </option> 84 + </select> 85 + <button class="btn flex gap-2 items-center" type="submit"> 86 + {{ i "check" "size-4" }} 87 + {{ i "loader-circle" "w-4 h-4 animate-spin hidden group-[.htmx-request]:inline" }} 88 + </button> 89 + </form> 90 + </div> 91 + </div> 92 + </div> 93 {{ end }}
+11 -2
appview/settings/settings.go
··· 82 83 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 84 user := s.OAuth.GetMultiAccountUser(r) 85 86 - s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ 87 LoggedInUser: user, 88 Tabs: settingsTabs, 89 Tab: "profile", 90 - }) 91 } 92 93 func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
··· 82 83 func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { 84 user := s.OAuth.GetMultiAccountUser(r) 85 + profile, err := db.GetProfile(s.Db, user.Did()) 86 + if err != nil { 87 + log.Printf("failed to get profile to check punchcard settings: %s", err) 88 + } 89 90 + params := pages.UserProfileSettingsParams{ 91 LoggedInUser: user, 92 Tabs: settingsTabs, 93 Tab: "profile", 94 + } 95 + if profile != nil { 96 + params.PunchcardSetting = profile.PunchardSetting 97 + } 98 + 99 + s.Pages.UserProfileSettings(w, params) 100 } 101 102 func (s *Settings) notificationsSettings(w http.ResponseWriter, r *http.Request) {
+60 -3
appview/state/profile.go
··· 157 } 158 } 159 160 - timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) 161 if err != nil { 162 l.Error("failed to create timeline", "err", err) 163 } 164 165 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 166 - LoggedInUser: s.oauth.GetMultiAccountUser(r), 167 Card: profile, 168 Repos: pinnedRepos, 169 CollaboratingRepos: pinnedCollaboratingRepos, 170 ProfileTimeline: timeline, 171 }) 172 } 173 ··· 404 } 405 406 func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 407 - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) 408 if err != nil { 409 return nil, err 410 } ··· 728 AllRepos: allRepos, 729 }) 730 }
··· 157 } 158 } 159 160 + loggedInUser := s.oauth.GetMultiAccountUser(r) 161 + 162 + forProfile, err := db.GetProfile(s.db, profile.UserDid) 163 + if err != nil { 164 + l.Error("failed to get for profile to check punchcard settings", "err", err) 165 + } 166 + requesterProfile, err := db.GetProfile(s.db, loggedInUser.Did()) 167 + if err != nil { 168 + l.Error("failed to get requester profile to check punchcard settings", "err", err) 169 + } 170 + 171 + showPunchcard := true 172 + if forProfile != nil && forProfile.PunchardSetting == models.ProfilePunchcardOptionHideMine { 173 + showPunchcard = false 174 + } 175 + if requesterProfile != nil && requesterProfile.PunchardSetting == models.ProfilePunchcardOptionHideAll { 176 + showPunchcard = false 177 + } 178 + 179 + timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid, showPunchcard) 180 if err != nil { 181 l.Error("failed to create timeline", "err", err) 182 } 183 184 + loggedInUserProfile, err := db.GetProfile(s.db, loggedInUser.Did()) 185 + if err != nil { 186 + l.Error("failed to get logged in user profile to check punchcard settings", "err", err) 187 + } 188 + 189 + if loggedInUserProfile != nil && loggedInUserProfile.PunchardSetting == models.ProfilePunchcardOptionHideAll { 190 + 191 + } 192 + 193 s.pages.ProfileOverview(w, pages.ProfileOverviewParams{ 194 + LoggedInUser: loggedInUser, 195 Card: profile, 196 Repos: pinnedRepos, 197 CollaboratingRepos: pinnedCollaboratingRepos, 198 ProfileTimeline: timeline, 199 + ShowPunchcard: showPunchcard, 200 }) 201 } 202 ··· 433 } 434 435 func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { 436 + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String(), false) 437 if err != nil { 438 return nil, err 439 } ··· 757 AllRepos: allRepos, 758 }) 759 } 760 + 761 + func (s *State) UpdateProfilePunchcardSetting(w http.ResponseWriter, r *http.Request) { 762 + err := r.ParseForm() 763 + if err != nil { 764 + log.Println("invalid profile update form", err) 765 + return 766 + } 767 + user := s.oauth.GetUser(r) 768 + 769 + profile, err := db.GetProfile(s.db, user.Did) 770 + if err != nil { 771 + log.Printf("getting profile data for %s: %s", user.Did, err) 772 + } 773 + 774 + if profile == nil { 775 + return 776 + } 777 + 778 + punchcard := r.Form.Get("punchcard-setting") 779 + 780 + err = db.SetProfilePunchcardStatus(s.db, profile.Did, models.ProfilePunchcardFromString(punchcard)) 781 + if err != nil { 782 + log.Println("failed to update profile", err) 783 + return 784 + } 785 + 786 + s.pages.HxRefresh(w) 787 + }
+1
appview/state/router.go
··· 165 r.Get("/edit-pins", s.EditPinsFragment) 166 r.Post("/bio", s.UpdateProfileBio) 167 r.Post("/pins", s.UpdateProfilePins) 168 }) 169 170 r.Mount("/settings", s.SettingsRouter())
··· 165 r.Get("/edit-pins", s.EditPinsFragment) 166 r.Post("/bio", s.UpdateProfileBio) 167 r.Post("/pins", s.UpdateProfilePins) 168 + r.Post("/punchcard", s.UpdateProfilePunchcardSetting) 169 }) 170 171 r.Mount("/settings", s.SettingsRouter())

History

1 round 1 comment
sign up or login to add to the discussion
willdot.net submitted #0
1 commit
expand
appview: allow users to decide how the profile punchcard is displayed
merge conflicts detected
expand
  • appview/db/db.go:1078
  • appview/db/profile.go:353
  • appview/models/profile.go:7
  • appview/pages/pages.go:337
  • appview/settings/settings.go:82
  • appview/state/profile.go:728
  • appview/state/router.go:165
expand 1 comment

Temporarily on hold. Need to use the same kinda pattern for storing the preference in the DB as https://tangled.org/tangled.org/core/pulls/991 so waiting until that’s approved etc before copying the pattern