From 2482a23f71f8a3acca4292dd6339f189005932e3 Mon Sep 17 00:00:00 2001 From: Will Andrews Date: Mon, 23 Feb 2026 14:50:23 +0000 Subject: [PATCH] appview: allow users to set their preferences for the punchcard being displayed Signed-off-by: Will Andrews --- appview/db/db.go | 7 ++ appview/db/preferences.go | 52 +++++++++++++++ appview/db/profile.go | 40 +++++------ appview/models/preferences.go | 8 +++ appview/pages/pages.go | 6 +- .../pages/templates/layouts/profilebase.html | 4 +- .../templates/user/settings/profile.html | 23 +++++++ appview/settings/settings.go | 8 ++- appview/state/profile.go | 66 ++++++++++++++++++- appview/state/router.go | 1 + 10 files changed, 190 insertions(+), 25 deletions(-) create mode 100644 appview/db/preferences.go create mode 100644 appview/models/preferences.go diff --git a/appview/db/db.go b/appview/db/db.go index caffad15..f7d30325 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -601,6 +601,13 @@ func Make(ctx context.Context, dbPath string) (*DB, error) { name text unique ); + create table if not exists punchcard_preferences ( + id integer primary key autoincrement, + user_did text not null unique, + hide_mine integer default 0, + hide_others integer default 0 + ); + -- indexes for better performance create index if not exists idx_notifications_recipient_created on notifications(recipient_did, created desc); create index if not exists idx_notifications_recipient_read on notifications(recipient_did, read); diff --git a/appview/db/preferences.go b/appview/db/preferences.go new file mode 100644 index 00000000..e1bca2af --- /dev/null +++ b/appview/db/preferences.go @@ -0,0 +1,52 @@ +package db + +import ( + "database/sql" + + "tangled.org/core/appview/models" +) + +func GetPunchcardPreference(e Execer, did string) (models.PunchcardPreference, error) { + preference := models.PunchcardPreference{ + Did: did, + } + + hideMine := 0 + hideOthers := 0 + + err := e.QueryRow( + `select id, hide_mine, hide_others from punchcard_preferences where user_did = ?`, + did, + ).Scan(&preference.ID, &hideMine, &hideOthers) + if err == sql.ErrNoRows { + return preference, nil + } + + preference.HideMine = hideMine > 0 + preference.HideOthers = hideOthers > 0 + + if err != nil { + return preference, err + } + + return preference, nil +} + +func UpsertPunchcardPreference(e Execer, did string, hideMine, hideOthers bool) error { + _, err := e.Exec( + `insert or replace into punchcard_preferences ( + user_did, + hide_mine, + hide_others + ) + values (?, ?, ?)`, + did, + hideMine, + hideOthers, + ) + if err != nil { + return err + } + + return nil +} diff --git a/appview/db/profile.go b/appview/db/profile.go index fdc5d993..25569af2 100644 --- a/appview/db/profile.go +++ b/appview/db/profile.go @@ -16,7 +16,7 @@ import ( const TimeframeMonths = 7 -func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, error) { +func MakeProfileTimeline(e Execer, forDid string, includePunchcard bool) (*models.ProfileTimeline, error) { timeline := models.ProfileTimeline{ ByMonth: make([]models.ByMonth, TimeframeMonths), } @@ -98,27 +98,29 @@ func MakeProfileTimeline(e Execer, forDid string) (*models.ProfileTimeline, erro }) } - punchcard, err := MakePunchcard( - e, - orm.FilterEq("did", forDid), - orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), - ) - if err != nil { - return nil, fmt.Errorf("error getting commits by did: %w", err) - } - for _, punch := range punchcard.Punches { - if punch.Date.After(now) { - continue + if includePunchcard { + punchcard, err := MakePunchcard( + e, + orm.FilterEq("did", forDid), + orm.FilterGte("date", time.Now().AddDate(0, -TimeframeMonths, 0)), + ) + if err != nil { + return nil, fmt.Errorf("error getting commits by did: %w", err) } + for _, punch := range punchcard.Punches { + if punch.Date.After(now) { + continue + } - monthsAgo := monthsBetween(punch.Date, now) - if monthsAgo >= TimeframeMonths { - // shouldn't happen; but times are weird - continue - } + monthsAgo := monthsBetween(punch.Date, now) + if monthsAgo >= TimeframeMonths { + // shouldn't happen; but times are weird + continue + } - idx := monthsAgo - timeline.ByMonth[idx].Commits += punch.Count + idx := monthsAgo + timeline.ByMonth[idx].Commits += punch.Count + } } return &timeline, nil diff --git a/appview/models/preferences.go b/appview/models/preferences.go new file mode 100644 index 00000000..850621b1 --- /dev/null +++ b/appview/models/preferences.go @@ -0,0 +1,8 @@ +package models + +type PunchcardPreference struct { + ID int + Did string + HideMine bool + HideOthers bool +} diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 4b77b34c..f95d0211 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -359,8 +359,9 @@ func (p *Pages) GoodFirstIssues(w io.Writer, params GoodFirstIssuesParams) error } type UserProfileSettingsParams struct { - LoggedInUser *oauth.MultiAccountUser - Tab string + LoggedInUser *oauth.MultiAccountUser + Tab string + PunchcardPreference models.PunchcardPreference } func (p *Pages) UserProfileSettings(w io.Writer, params UserProfileSettingsParams) error { @@ -557,6 +558,7 @@ type ProfileOverviewParams struct { ProfileTimeline *models.ProfileTimeline Card *ProfileCard Active string + ShowPunchcard bool } func (p *Pages) ProfileOverview(w io.Writer, params ProfileOverviewParams) error { diff --git a/appview/pages/templates/layouts/profilebase.html b/appview/pages/templates/layouts/profilebase.html index 30ee9622..349edddd 100644 --- a/appview/pages/templates/layouts/profilebase.html +++ b/appview/pages/templates/layouts/profilebase.html @@ -52,7 +52,9 @@
{{ template "user/fragments/profileCard" .Card }} - {{ block "punchcard" .Card.Punchcard }} {{ end }} + {{ if .ShowPunchcard }} + {{ block "punchcard" .Card.Punchcard }} {{ end }} + {{ end }}
diff --git a/appview/pages/templates/user/settings/profile.html b/appview/pages/templates/user/settings/profile.html index 5c9a874e..b65990f1 100644 --- a/appview/pages/templates/user/settings/profile.html +++ b/appview/pages/templates/user/settings/profile.html @@ -59,4 +59,27 @@ +
+
+
+
+ Punchcard settings +
+
+
+ + +
+
+ + +
+ +
+
+
+
{{ end }} diff --git a/appview/settings/settings.go b/appview/settings/settings.go index ed76911f..996571da 100644 --- a/appview/settings/settings.go +++ b/appview/settings/settings.go @@ -70,8 +70,14 @@ func (s *Settings) Router() http.Handler { func (s *Settings) profileSettings(w http.ResponseWriter, r *http.Request) { user := s.OAuth.GetMultiAccountUser(r) + punchcardPreferences, err := db.GetPunchcardPreference(s.Db, user.Did()) + if err != nil { + log.Printf("failed to get users punchcard preferences: %s", err) + } + s.Pages.UserProfileSettings(w, pages.UserProfileSettingsParams{ - LoggedInUser: user, + LoggedInUser: user, + PunchcardPreference: punchcardPreferences, }) } diff --git a/appview/state/profile.go b/appview/state/profile.go index 67587abd..392b0b04 100644 --- a/appview/state/profile.go +++ b/appview/state/profile.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "log/slog" "net/http" "slices" "strings" @@ -164,7 +165,11 @@ func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { } } - timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid) + loggedInUser := s.oauth.GetMultiAccountUser(r) + + showPunchcard := checkIfPunchcardShouldShow(s.db, l, profile.UserDid, loggedInUser.Did()) + + timeline, err := db.MakeProfileTimeline(s.db, profile.UserDid, showPunchcard) if err != nil { l.Error("failed to create timeline", "err", err) } @@ -175,9 +180,39 @@ func (s *State) profileOverview(w http.ResponseWriter, r *http.Request) { Repos: pinnedRepos, CollaboratingRepos: pinnedCollaboratingRepos, ProfileTimeline: timeline, + ShowPunchcard: showPunchcard, }) } +func checkIfPunchcardShouldShow(e db.Execer, l *slog.Logger, targetDid, requesterDid string) bool { + targetPunchcardPreferences, err := db.GetPunchcardPreference(e, targetDid) + if err != nil { + l.Error("failed to get target users punchcard preferences", "err", err) + return true + } + + requesterPunchcardPreferences, err := db.GetPunchcardPreference(e, requesterDid) + if err != nil { + l.Error("failed to get requester users punchcard preferences", "err", err) + return true + } + + showPunchcard := true + + // looking at their own profile + if targetDid == requesterDid { + if targetPunchcardPreferences.HideMine { + return false + } + return true + } + + if targetPunchcardPreferences.HideMine || requesterPunchcardPreferences.HideOthers { + showPunchcard = false + } + return showPunchcard +} + func (s *State) reposPage(w http.ResponseWriter, r *http.Request) { l := s.logger.With("handler", "reposPage") @@ -411,7 +446,7 @@ func (s *State) AtomFeedPage(w http.ResponseWriter, r *http.Request) { } func (s *State) getProfileFeed(ctx context.Context, id *identity.Identity) (*feeds.Feed, error) { - timeline, err := db.MakeProfileTimeline(s.db, id.DID.String()) + timeline, err := db.MakeProfileTimeline(s.db, id.DID.String(), false) if err != nil { return nil, err } @@ -936,3 +971,30 @@ func (s *State) RemoveProfileAvatar(w http.ResponseWriter, r *http.Request) { s.pages.HxRedirect(w, r.Header.Get("Referer")) } + +func (s *State) UpdateProfilePunchcardSetting(w http.ResponseWriter, r *http.Request) { + err := r.ParseForm() + if err != nil { + log.Println("invalid profile update form", err) + return + } + user := s.oauth.GetUser(r) + + hideOthers := false + hideMine := false + + if r.Form.Get("hideMine") == "on" { + hideMine = true + } + if r.Form.Get("hideOthers") == "on" { + hideOthers = true + } + + err = db.UpsertPunchcardPreference(s.db, user.Did, hideMine, hideOthers) + if err != nil { + log.Println("failed to update punchcard preferences", err) + return + } + + s.pages.HxRefresh(w) +} diff --git a/appview/state/router.go b/appview/state/router.go index 56d4b374..37533da1 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -167,6 +167,7 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Post("/pins", s.UpdateProfilePins) r.Post("/avatar", s.UploadProfileAvatar) r.Delete("/avatar", s.RemoveProfileAvatar) + r.Post("/punchcard", s.UpdateProfilePunchcardSetting) }) r.Mount("/settings", s.SettingsRouter()) -- 2.51.2