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>

author
oppiliappan
committer
Tangled
date (Jul 16, 2026, 4:33 PM +0300) commit 6a8c2960 parent f169fa4e change-id wzqxxtrn
+243 -6
+1 -6
appview/models/onboarding.go
··· 44 44 if o == nil || o.Status != OnboardingInProgress { 45 45 return OnboardingProgress{} 46 46 } 47 - // Percent reflects completed steps out of the total, so the bar only fills 48 - // as steps are finished (e.g. on the last step it is not yet 100%). 49 47 total := OnboardingStepDone 50 - percent := o.Step * 100 / total 51 - if percent > 100 { 52 - percent = 100 53 - } 48 + percent := min(o.Step*100/total, 100) 54 49 return OnboardingProgress{ 55 50 Active: true, 56 51 Step: o.Step,
+230
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 step == models.OnboardingStepRepo { 53 + if err := db.CompleteOnboarding(s.db, did); err != nil { 54 + s.logger.Error("failed to complete onboarding", "did", did, "err", err) 55 + } 56 + s.pages.HxRedirect(w, onboardingStepURL(step)) 57 + return 58 + } 59 + 60 + if err := db.AdvanceOnboardingStep(s.db, did, step); err != nil { 61 + s.logger.Error("failed to advance onboarding", "did", did, "err", err) 62 + } 63 + s.pages.HxRedirect(w, onboardingStepURL(step)) 64 + } 65 + 66 + func (s *State) OnboardingSkip(w http.ResponseWriter, r *http.Request) { 67 + did := s.oauth.GetDid(r) 68 + if err := db.SkipOnboarding(s.db, did); err != nil { 69 + s.logger.Error("failed to skip onboarding", "did", did, "err", err) 70 + } 71 + s.pages.HxRedirect(w, "/") 72 + } 73 + 74 + func (s *State) OnboardingComplete(w http.ResponseWriter, r *http.Request) { 75 + did := s.oauth.GetDid(r) 76 + if err := db.CompleteOnboarding(s.db, did); err != nil { 77 + s.logger.Error("failed to complete onboarding", "did", did, "err", err) 78 + } 79 + s.pages.HxRedirect(w, "/") 80 + } 81 + 82 + func (s *State) OnboardingProfile(w http.ResponseWriter, r *http.Request) { 83 + user := s.oauth.GetMultiAccountUser(r) 84 + 85 + profile, err := db.GetProfile(s.db, user.Did) 86 + if err != nil { 87 + s.logger.Error("getting profile data", "did", user.Did, "err", err) 88 + } 89 + if profile == nil { 90 + profile = &models.Profile{Did: user.Did} 91 + } 92 + 93 + var alsoKnownAs []string 94 + if ident, err := s.idResolver.ResolveIdent(r.Context(), user.Did); err == nil { 95 + alsoKnownAs = ident.AlsoKnownAs 96 + } 97 + 98 + bp := pages.BaseParamsFromContext(r.Context()) 99 + s.pages.Onboarding(w, pages.OnboardingParams{ 100 + BaseParams: bp, 101 + Step: models.OnboardingStepProfile, 102 + EditBio: pages.EditBioParams{ 103 + BaseParams: bp, 104 + Profile: profile, 105 + AlsoKnownAs: alsoKnownAs, 106 + Action: "/welcome/profile", 107 + }, 108 + }) 109 + } 110 + 111 + func (s *State) OnboardingSaveProfile(w http.ResponseWriter, r *http.Request) { 112 + did := s.oauth.GetDid(r) 113 + 114 + if err := r.ParseForm(); err != nil { 115 + s.pages.Notice(w, "update-profile", "Invalid form.") 116 + return 117 + } 118 + 119 + profile, err := s.bioFormToProfile(r) 120 + if err != nil { 121 + s.pages.Notice(w, "update-profile", err.Error()) 122 + return 123 + } 124 + 125 + if err := s.writeProfile(r, profile); err != nil { 126 + s.logger.Error("onboarding: failed to write profile", "did", did, "err", err) 127 + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") 128 + return 129 + } 130 + 131 + if err := db.AdvanceOnboardingStep(s.db, did, models.OnboardingStepSocial); err != nil { 132 + s.logger.Error("failed to advance onboarding", "did", did, "err", err) 133 + } 134 + s.pages.HxRedirect(w, "/welcome/social") 135 + } 136 + 137 + func (s *State) OnboardingKeys(w http.ResponseWriter, r *http.Request) { 138 + user := s.oauth.GetMultiAccountUser(r) 139 + 140 + pubKeys, err := db.GetPublicKeysForDid(s.db, user.Did) 141 + if err != nil { 142 + s.logger.Error("getting public keys", "did", user.Did, "err", err) 143 + } 144 + 145 + s.pages.Onboarding(w, pages.OnboardingParams{ 146 + BaseParams: pages.BaseParamsFromContext(r.Context()), 147 + Step: models.OnboardingStepKeys, 148 + PubKeys: pubKeys, 149 + }) 150 + } 151 + 152 + const onboardingSocialLimit = 4 153 + 154 + func (s *State) OnboardingSocial(w http.ResponseWriter, r *http.Request) { 155 + user := s.oauth.GetMultiAccountUser(r) 156 + bp := pages.BaseParamsFromContext(r.Context()) 157 + 158 + var people []pages.FollowCard 159 + mostFollowed, err := db.GetMostFollowed(s.db, onboardingSocialLimit+2) 160 + if err != nil { 161 + s.logger.Error("failed to get most followed", "err", err) 162 + } 163 + var dids []string 164 + for _, did := range mostFollowed { 165 + if did == user.Did { 166 + continue 167 + } 168 + dids = append(dids, did) 169 + if len(dids) >= onboardingSocialLimit { 170 + break 171 + } 172 + } 173 + if len(dids) > 0 { 174 + statuses, _ := db.GetFollowStatuses(s.db, user.Did, dids) 175 + counts, _ := db.GetFollowerFollowingCounts(s.db, dids) 176 + for _, did := range dids { 177 + profile, _ := db.GetProfile(s.db, did) 178 + if profile == nil { 179 + profile = &models.Profile{Did: did} 180 + } 181 + people = append(people, pages.FollowCard{ 182 + BaseParams: bp, 183 + UserDid: did, 184 + Profile: profile, 185 + FollowStatus: statuses[did], 186 + FollowersCount: counts[did].Followers, 187 + FollowingCount: counts[did].Following, 188 + }) 189 + } 190 + } 191 + 192 + repos, err := db.GetTopStarredReposLastWeek(s.db) 193 + if err != nil { 194 + s.logger.Error("failed to get trending repos", "err", err) 195 + } 196 + trending := make([]models.Repo, 0, onboardingSocialLimit) 197 + for _, repo := range repos { 198 + if repo.RepoDid == "" { 199 + continue 200 + } 201 + trending = append(trending, repo) 202 + if len(trending) >= onboardingSocialLimit { 203 + break 204 + } 205 + } 206 + 207 + starStatuses := map[string]bool{} 208 + if len(trending) > 0 { 209 + repoDids := make([]string, 0, len(trending)) 210 + for _, repo := range trending { 211 + repoDids = append(repoDids, repo.RepoDid) 212 + } 213 + starStatuses, _ = db.GetStarStatuses(s.db, user.Did, repoDids) 214 + } 215 + 216 + s.pages.Onboarding(w, pages.OnboardingParams{ 217 + BaseParams: bp, 218 + Step: models.OnboardingStepSocial, 219 + People: people, 220 + TrendingRepos: trending, 221 + StarStatuses: starStatuses, 222 + }) 223 + } 224 + 225 + func (s *State) OnboardingRepo(w http.ResponseWriter, r *http.Request) { 226 + s.pages.Onboarding(w, pages.OnboardingParams{ 227 + BaseParams: pages.BaseParamsFromContext(r.Context()), 228 + Step: models.OnboardingStepRepo, 229 + }) 230 + }
+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