diff --git a/appview/focus/focus.go b/appview/focus/focus.go new file mode 100644 index 00000000..fa527291 --- /dev/null +++ b/appview/focus/focus.go @@ -0,0 +1,127 @@ +package focus + +import ( + "log/slog" + "net/http" + "strconv" + + "github.com/go-chi/chi/v5" + "tangled.org/core/appview/db" + "tangled.org/core/appview/middleware" + "tangled.org/core/appview/oauth" + "tangled.org/core/appview/pages" + "tangled.org/core/idresolver" +) + +type Focus struct { + db *db.DB + oauth *oauth.OAuth + pages *pages.Pages + logger *slog.Logger + resolver *idresolver.Resolver +} + +func New(database *db.DB, o *oauth.OAuth, res *idresolver.Resolver, p *pages.Pages, logger *slog.Logger) *Focus { + return &Focus{ + db: database, + oauth: o, + pages: p, + resolver: res, + logger: logger, + } +} + +func (f *Focus) Router(mw *middleware.Middleware) http.Handler { + r := chi.NewRouter() + r.Use(middleware.AuthMiddleware(f.oauth)) + r.Post("/begin", f.BeginFocus) + r.Post("/end", f.EndFocus) + r.Post("/next", f.FocusNext) + return r +} + +// BeginFocus activates focus mode and redirects the user to the oldest unread +// focus-eligible notification. +func (f *Focus) BeginFocus(w http.ResponseWriter, r *http.Request) { + l := f.logger.With("handler", "BeginFocus") + did := f.oauth.GetDid(r) + + if err := db.BeginFocus(f.db, did); err != nil { + l.Error("failed to begin focus", "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + item, err := db.GetNextFocusItem(f.db, did) + if err != nil { + l.Error("failed to get first focus item", "err", err) + _ = db.EndFocus(f.db, did) + http.Redirect(w, r, "/notifications", http.StatusSeeOther) + return + } + if item == nil { + _ = db.EndFocus(f.db, did) + http.Redirect(w, r, "/notifications", http.StatusSeeOther) + return + } + + target := item.URL(f.resolver) + if target == "" { + _ = db.EndFocus(f.db, did) + http.Redirect(w, r, "/notifications", http.StatusSeeOther) + return + } + + http.Redirect(w, r, target, http.StatusSeeOther) +} + +// EndFocus deactivates focus mode and redirects to /notifications. +func (f *Focus) EndFocus(w http.ResponseWriter, r *http.Request) { + l := f.logger.With("handler", "EndFocus") + did := f.oauth.GetDid(r) + + if err := db.EndFocus(f.db, did); err != nil { + l.Error("failed to end focus", "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/notifications", http.StatusSeeOther) +} + +// FocusNext marks the current focus notification as read and redirects to the +// next focus item. If the queue is empty, focus mode is ended. +func (f *Focus) FocusNext(w http.ResponseWriter, r *http.Request) { + l := f.logger.With("handler", "FocusNext") + did := f.oauth.GetDid(r) + + // Mark the current item read so it leaves the queue. + if currentIDStr := r.FormValue("current_id"); currentIDStr != "" { + if currentID, err := strconv.ParseInt(currentIDStr, 10, 64); err == nil && currentID > 0 { + if err := db.MarkNotificationRead(f.db, currentID, did); err != nil { + l.Warn("failed to mark notification read", "id", currentID, "err", err) + } + } + } + + item, err := db.GetNextFocusItem(f.db, did) + if err != nil { + l.Error("failed to get next focus item", "err", err) + http.Error(w, "internal error", http.StatusInternalServerError) + return + } + if item == nil { + _ = db.EndFocus(f.db, did) + http.Redirect(w, r, "/notifications", http.StatusSeeOther) + return + } + + target := item.URL(f.resolver) + if target == "" { + _ = db.EndFocus(f.db, did) + http.Redirect(w, r, "/notifications", http.StatusSeeOther) + return + } + + http.Redirect(w, r, target, http.StatusSeeOther) +}