diff --git a/appview/models/onboarding.go b/appview/models/onboarding.go index 22d76fc5..44076eeb 100644 --- a/appview/models/onboarding.go +++ b/appview/models/onboarding.go @@ -44,13 +44,8 @@ func (o *Onboarding) Progress() OnboardingProgress { if o == nil || o.Status != OnboardingInProgress { return OnboardingProgress{} } - // Percent reflects completed steps out of the total, so the bar only fills - // as steps are finished (e.g. on the last step it is not yet 100%). total := OnboardingStepDone - percent := o.Step * 100 / total - if percent > 100 { - percent = 100 - } + percent := min(o.Step*100/total, 100) return OnboardingProgress{ Active: true, Step: o.Step, diff --git a/appview/state/onboarding.go b/appview/state/onboarding.go new file mode 100644 index 00000000..7df998b6 --- /dev/null +++ b/appview/state/onboarding.go @@ -0,0 +1,230 @@ +package state + +import ( + "net/http" + "strconv" + + "tangled.org/core/appview/db" + "tangled.org/core/appview/models" + "tangled.org/core/appview/pages" +) + +func onboardingStepURL(step int) string { + switch step { + case models.OnboardingStepProfile: + return "/welcome/profile" + case models.OnboardingStepKeys: + return "/welcome/keys" + case models.OnboardingStepSocial: + return "/welcome/social" + case models.OnboardingStepRepo: + return "/welcome/repo" + default: + return "/" + } +} + +func (s *State) OnboardingResume(w http.ResponseWriter, r *http.Request) { + did := s.oauth.GetDid(r) + ob, err := db.GetOnboarding(s.db, did) + if err != nil { + s.logger.Error("failed to get onboarding", "did", did, "err", err) + } + if ob == nil || ob.Status != models.OnboardingInProgress { + http.Redirect(w, r, "/", http.StatusFound) + return + } + http.Redirect(w, r, onboardingStepURL(ob.Step), http.StatusFound) +} + +func (s *State) OnboardingNext(w http.ResponseWriter, r *http.Request) { + did := s.oauth.GetDid(r) + step, _ := strconv.Atoi(r.FormValue("step")) + + if step >= models.OnboardingStepDone { + if err := db.CompleteOnboarding(s.db, did); err != nil { + s.logger.Error("failed to complete onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, "/") + return + } + + if step == models.OnboardingStepRepo { + if err := db.CompleteOnboarding(s.db, did); err != nil { + s.logger.Error("failed to complete onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, onboardingStepURL(step)) + return + } + + if err := db.AdvanceOnboardingStep(s.db, did, step); err != nil { + s.logger.Error("failed to advance onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, onboardingStepURL(step)) +} + +func (s *State) OnboardingSkip(w http.ResponseWriter, r *http.Request) { + did := s.oauth.GetDid(r) + if err := db.SkipOnboarding(s.db, did); err != nil { + s.logger.Error("failed to skip onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, "/") +} + +func (s *State) OnboardingComplete(w http.ResponseWriter, r *http.Request) { + did := s.oauth.GetDid(r) + if err := db.CompleteOnboarding(s.db, did); err != nil { + s.logger.Error("failed to complete onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, "/") +} + +func (s *State) OnboardingProfile(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetMultiAccountUser(r) + + profile, err := db.GetProfile(s.db, user.Did) + if err != nil { + s.logger.Error("getting profile data", "did", user.Did, "err", err) + } + if profile == nil { + profile = &models.Profile{Did: user.Did} + } + + var alsoKnownAs []string + if ident, err := s.idResolver.ResolveIdent(r.Context(), user.Did); err == nil { + alsoKnownAs = ident.AlsoKnownAs + } + + bp := pages.BaseParamsFromContext(r.Context()) + s.pages.Onboarding(w, pages.OnboardingParams{ + BaseParams: bp, + Step: models.OnboardingStepProfile, + EditBio: pages.EditBioParams{ + BaseParams: bp, + Profile: profile, + AlsoKnownAs: alsoKnownAs, + Action: "/welcome/profile", + }, + }) +} + +func (s *State) OnboardingSaveProfile(w http.ResponseWriter, r *http.Request) { + did := s.oauth.GetDid(r) + + if err := r.ParseForm(); err != nil { + s.pages.Notice(w, "update-profile", "Invalid form.") + return + } + + profile, err := s.bioFormToProfile(r) + if err != nil { + s.pages.Notice(w, "update-profile", err.Error()) + return + } + + if err := s.writeProfile(r, profile); err != nil { + s.logger.Error("onboarding: failed to write profile", "did", did, "err", err) + s.pages.Notice(w, "update-profile", "Failed to update profile, try again later.") + return + } + + if err := db.AdvanceOnboardingStep(s.db, did, models.OnboardingStepSocial); err != nil { + s.logger.Error("failed to advance onboarding", "did", did, "err", err) + } + s.pages.HxRedirect(w, "/welcome/social") +} + +func (s *State) OnboardingKeys(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetMultiAccountUser(r) + + pubKeys, err := db.GetPublicKeysForDid(s.db, user.Did) + if err != nil { + s.logger.Error("getting public keys", "did", user.Did, "err", err) + } + + s.pages.Onboarding(w, pages.OnboardingParams{ + BaseParams: pages.BaseParamsFromContext(r.Context()), + Step: models.OnboardingStepKeys, + PubKeys: pubKeys, + }) +} + +const onboardingSocialLimit = 4 + +func (s *State) OnboardingSocial(w http.ResponseWriter, r *http.Request) { + user := s.oauth.GetMultiAccountUser(r) + bp := pages.BaseParamsFromContext(r.Context()) + + var people []pages.FollowCard + mostFollowed, err := db.GetMostFollowed(s.db, onboardingSocialLimit+2) + if err != nil { + s.logger.Error("failed to get most followed", "err", err) + } + var dids []string + for _, did := range mostFollowed { + if did == user.Did { + continue + } + dids = append(dids, did) + if len(dids) >= onboardingSocialLimit { + break + } + } + if len(dids) > 0 { + statuses, _ := db.GetFollowStatuses(s.db, user.Did, dids) + counts, _ := db.GetFollowerFollowingCounts(s.db, dids) + for _, did := range dids { + profile, _ := db.GetProfile(s.db, did) + if profile == nil { + profile = &models.Profile{Did: did} + } + people = append(people, pages.FollowCard{ + BaseParams: bp, + UserDid: did, + Profile: profile, + FollowStatus: statuses[did], + FollowersCount: counts[did].Followers, + FollowingCount: counts[did].Following, + }) + } + } + + repos, err := db.GetTopStarredReposLastWeek(s.db) + if err != nil { + s.logger.Error("failed to get trending repos", "err", err) + } + trending := make([]models.Repo, 0, onboardingSocialLimit) + for _, repo := range repos { + if repo.RepoDid == "" { + continue + } + trending = append(trending, repo) + if len(trending) >= onboardingSocialLimit { + break + } + } + + starStatuses := map[string]bool{} + if len(trending) > 0 { + repoDids := make([]string, 0, len(trending)) + for _, repo := range trending { + repoDids = append(repoDids, repo.RepoDid) + } + starStatuses, _ = db.GetStarStatuses(s.db, user.Did, repoDids) + } + + s.pages.Onboarding(w, pages.OnboardingParams{ + BaseParams: bp, + Step: models.OnboardingStepSocial, + People: people, + TrendingRepos: trending, + StarStatuses: starStatuses, + }) +} + +func (s *State) OnboardingRepo(w http.ResponseWriter, r *http.Request) { + s.pages.Onboarding(w, pages.OnboardingParams{ + BaseParams: pages.BaseParamsFromContext(r.Context()), + Step: models.OnboardingStepRepo, + }) +} diff --git a/appview/state/router.go b/appview/state/router.go index 37bac52e..b1d9b13f 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -270,6 +270,18 @@ func (s *State) StandardRouter(mw *middleware.Middleware) http.Handler { r.Post("/punchcard", s.UpdateProfilePunchcardSetting) }) + r.With(middleware.AuthMiddleware(s.oauth)).Route("/welcome", func(r chi.Router) { + r.Get("/", s.OnboardingResume) + r.Get("/profile", s.OnboardingProfile) + r.Post("/profile", s.OnboardingSaveProfile) + r.Get("/keys", s.OnboardingKeys) + r.Get("/social", s.OnboardingSocial) + r.Get("/repo", s.OnboardingRepo) + r.Post("/next", s.OnboardingNext) + r.Post("/skip", s.OnboardingSkip) + r.Post("/complete", s.OnboardingComplete) + }) + r.Mount("/settings", s.SettingsRouter()) r.Mount("/strings", s.StringsRouter(mw))