Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

appview/state: add handlers and routers for onboarding

Signed-off-by: oppiliappan <me@oppi.li>

oppiliappan (Jul 7, 2026, 5:27 PM +0100) f596566d 8a3b1ca5

+239
+227
appview/state/onboarding.go
··· 1 + package state 2 + 3 + import ( 4 + "net/http" 5 + "strconv" 6 + 7 + "tangled.org/core/appview/db" 8 + "tangled.org/core/appview/models" 9 + "tangled.org/core/appview/pages" 10 + ) 11 + 12 + func onboardingStepURL(step int) string { 13 + switch step { 14 + case models.OnboardingStepProfile: 15 + return "/welcome/profile" 16 + case models.OnboardingStepKeys: 17 + return "/welcome/keys" 18 + case models.OnboardingStepSocial: 19 + return "/welcome/social" 20 + case models.OnboardingStepRepo: 21 + return "/welcome/repo" 22 + default: 23 + return "/" 24 + } 25 + } 26 + 27 + func (s *State) OnboardingResume(w http.ResponseWriter, r *http.Request) { 28 + did := s.oauth.GetDid(r) 29 + ob, err := db.GetOnboarding(s.db, did) 30 + if err != nil { 31 + s.logger.Error("failed to get onboarding", "did", did, "err", err) 32 + } 33 + if ob == nil || ob.Status != models.OnboardingInProgress { 34 + http.Redirect(w, r, "/", http.StatusFound) 35 + return 36 + } 37 + http.Redirect(w, r, onboardingStepURL(ob.Step), http.StatusFound) 38 + } 39 + 40 + func (s *State) OnboardingNext(w http.ResponseWriter, r *http.Request) { 41 + did := s.oauth.GetDid(r) 42 + step, _ := strconv.Atoi(r.FormValue("step")) 43 + 44 + if step >= models.OnboardingStepDone { 45 + if err := db.CompleteOnboarding(s.db, did); err != nil { 46 + s.logger.Error("failed to complete onboarding", "did", did, "err", err) 47 + } 48 + s.pages.HxRedirect(w, "/") 49 + return 50 + } 51 + 52 + if err := db.AdvanceOnboardingStep(s.db, did, step); err != nil { 53 + s.logger.Error("failed to advance onboarding", "did", did, "err", err) 54 + } 55 + s.pages.HxRedirect(w, onboardingStepURL(step)) 56 + } 57 + 58 + func (s *State) OnboardingSkip(w http.ResponseWriter, r *http.Request) { 59 + did := s.oauth.GetDid(r) 60 + if err := db.SkipOnboarding(s.db, did); err != nil { 61 + s.logger.Error("failed to skip onboarding", "did", did, "err", err) 62 + } 63 + s.pages.HxRedirect(w, "/") 64 + } 65 + 66 + func (s *State) OnboardingComplete(w http.ResponseWriter, r *http.Request) { 67 + did := s.oauth.GetDid(r) 68 + if err := db.CompleteOnboarding(s.db, did); err != nil { 69 + s.logger.Error("failed to complete onboarding", "did", did, "err", err) 70 + } 71 + s.pages.HxRedirect(w, "/") 72 + } 73 + 74 + func (s *State) OnboardingProfile(w http.ResponseWriter, r *http.Request) { 75 + user := s.oauth.GetMultiAccountUser(r) 76 + 77 + profile, err := db.GetProfile(s.db, user.Did) 78 + if err != nil { 79 + s.logger.Error("getting profile data", "did", user.Did, "err", err) 80 + } 81 + if profile == nil { 82 + profile = &models.Profile{Did: user.Did} 83 + } 84 + 85 + var alsoKnownAs []string 86 + if ident, err := s.idResolver.ResolveIdent(r.Context(), user.Did); err == nil { 87 + alsoKnownAs = ident.AlsoKnownAs 88 + } 89 + 90 + bp := pages.BaseParamsFromContext(r.Context()) 91 + s.pages.Onboarding(w, pages.OnboardingParams{ 92 + BaseParams: bp, 93 + Step: models.OnboardingStepProfile, 94 + EditBio: pages.EditBioParams{ 95 + BaseParams: bp, 96 + Profile: profile, 97 + AlsoKnownAs: alsoKnownAs, 98 + Action: "/welcome/profile", 99 + }, 100 + }) 101 + } 102 + 103 + func (s *State) OnboardingSaveProfile(w http.ResponseWriter, r *http.Request) { 104 + did := s.oauth.GetDid(r) 105 + 106 + if err := r.ParseForm(); err != nil { 107 + s.pages.Notice(w, "update-profile", "Invalid form.") 108 + return 109 + } 110 + 111 + profile, err := s.bioFormToProfile(r) 112 + if err != nil { 113 + s.pages.Notice(w, "update-profile", err.Error()) 114 + return 115 + } 116 + 117 + if err := s.writeProfile(r, profile); err != nil { 118 + s.logger.Error("onboarding: failed to write profile", "did", did, "err", err) 119 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 120 + return 121 + } 122 + 123 + if err := db.AdvanceOnboardingStep(s.db, did, models.OnboardingStepSocial); err != nil { 124 + s.logger.Error("failed to advance onboarding", "did", did, "err", err) 125 + } 126 + s.pages.HxRedirect(w, "/welcome/social") 127 + } 128 + 129 + func (s *State) OnboardingKeys(w http.ResponseWriter, r *http.Request) { 130 + user := s.oauth.GetMultiAccountUser(r) 131 + 132 + pubKeys, err := db.GetPublicKeysForDid(s.db, user.Did) 133 + if err != nil { 134 + s.logger.Error("getting public keys", "did", user.Did, "err", err) 135 + } 136 + 137 + s.pages.Onboarding(w, pages.OnboardingParams{ 138 + BaseParams: pages.BaseParamsFromContext(r.Context()), 139 + Step: models.OnboardingStepKeys, 140 + PubKeys: pubKeys, 141 + }) 142 + } 143 + 144 + const onboardingSocialLimit = 4 145 + 146 + func (s *State) OnboardingSocial(w http.ResponseWriter, r *http.Request) { 147 + user := s.oauth.GetMultiAccountUser(r) 148 + bp := pages.BaseParamsFromContext(r.Context()) 149 + 150 + var people []pages.FollowCard 151 + mostFollowed, err := db.GetMostFollowed(s.db, onboardingSocialLimit+2) 152 + if err != nil { 153 + s.logger.Error("failed to get most followed", "err", err) 154 + } 155 + var dids []string 156 + for _, did := range mostFollowed { 157 + if did == user.Did { 158 + continue 159 + } 160 + dids = append(dids, did) 161 + if len(dids) >= onboardingSocialLimit { 162 + break 163 + } 164 + } 165 + if len(dids) > 0 { 166 + statuses, _ := db.GetFollowStatuses(s.db, user.Did, dids) 167 + counts, _ := db.GetFollowerFollowingCounts(s.db, dids) 168 + for _, did := range dids { 169 + profile, _ := db.GetProfile(s.db, did) 170 + if profile == nil { 171 + profile = &models.Profile{Did: did} 172 + } 173 + people = append(people, pages.FollowCard{ 174 + BaseParams: bp, 175 + UserDid: did, 176 + Profile: profile, 177 + FollowStatus: statuses[did], 178 + FollowersCount: counts[did].Followers, 179 + FollowingCount: counts[did].Following, 180 + }) 181 + } 182 + } 183 + 184 + repos, err := db.GetTopStarredReposLastWeek(s.db) 185 + if err != nil { 186 + s.logger.Error("failed to get trending repos", "err", err) 187 + } 188 + trending := make([]models.Repo, 0, onboardingSocialLimit) 189 + for _, repo := range repos { 190 + if repo.RepoDid == "" { 191 + continue 192 + } 193 + trending = append(trending, repo) 194 + if len(trending) >= onboardingSocialLimit { 195 + break 196 + } 197 + } 198 + 199 + starStatuses := map[string]bool{} 200 + if len(trending) > 0 { 201 + repoDids := make([]string, 0, len(trending)) 202 + for _, repo := range trending { 203 + repoDids = append(repoDids, repo.RepoDid) 204 + } 205 + starStatuses, _ = db.GetStarStatuses(s.db, user.Did, repoDids) 206 + } 207 + 208 + s.pages.Onboarding(w, pages.OnboardingParams{ 209 + BaseParams: bp, 210 + Step: models.OnboardingStepSocial, 211 + People: people, 212 + TrendingRepos: trending, 213 + StarStatuses: starStatuses, 214 + }) 215 + } 216 + 217 + func (s *State) OnboardingRepo(w http.ResponseWriter, r *http.Request) { 218 + did := s.oauth.GetDid(r) 219 + if err := db.CompleteOnboarding(s.db, did); err != nil { 220 + s.logger.Error("failed to complete onboarding", "did", did, "err", err) 221 + } 222 + 223 + s.pages.Onboarding(w, pages.OnboardingParams{ 224 + BaseParams: pages.BaseParamsFromContext(r.Context()), 225 + Step: models.OnboardingStepRepo, 226 + }) 227 + }
+12
appview/state/router.go
··· 270 270 r.Post("/punchcard", s.UpdateProfilePunchcardSetting) 271 271 }) 272 272 273 + r.With(middleware.AuthMiddleware(s.oauth)).Route("/welcome", func(r chi.Router) { 274 + r.Get("/", s.OnboardingResume) 275 + r.Get("/profile", s.OnboardingProfile) 276 + r.Post("/profile", s.OnboardingSaveProfile) 277 + r.Get("/keys", s.OnboardingKeys) 278 + r.Get("/social", s.OnboardingSocial) 279 + r.Get("/repo", s.OnboardingRepo) 280 + r.Post("/next", s.OnboardingNext) 281 + r.Post("/skip", s.OnboardingSkip) 282 + r.Post("/complete", s.OnboardingComplete) 283 + }) 284 + 273 285 r.Mount("/settings", s.SettingsRouter()) 274 286 r.Mount("/strings", s.StringsRouter(mw)) 275 287