Monorepo for Tangled tangled.org
2

Configure Feed

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

appview/focus: add web handlers for focus mode

- /focus/begin: begin focus mode
- /focus/end: end focus mode
- /focus/next: mark read & move to next focus item

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

author
oppiliappan
committer
Tangled
date (Jun 15, 2026, 5:28 PM +0300) commit 3366814b parent 9fadcd21 change-id zwxtlmys
+127
+127
appview/focus/focus.go
··· 1 + package focus 2 + 3 + import ( 4 + "log/slog" 5 + "net/http" 6 + "strconv" 7 + 8 + "github.com/go-chi/chi/v5" 9 + "tangled.org/core/appview/db" 10 + "tangled.org/core/appview/middleware" 11 + "tangled.org/core/appview/oauth" 12 + "tangled.org/core/appview/pages" 13 + "tangled.org/core/idresolver" 14 + ) 15 + 16 + type Focus struct { 17 + db *db.DB 18 + oauth *oauth.OAuth 19 + pages *pages.Pages 20 + logger *slog.Logger 21 + resolver *idresolver.Resolver 22 + } 23 + 24 + func New(database *db.DB, o *oauth.OAuth, res *idresolver.Resolver, p *pages.Pages, logger *slog.Logger) *Focus { 25 + return &Focus{ 26 + db: database, 27 + oauth: o, 28 + pages: p, 29 + resolver: res, 30 + logger: logger, 31 + } 32 + } 33 + 34 + func (f *Focus) Router(mw *middleware.Middleware) http.Handler { 35 + r := chi.NewRouter() 36 + r.Use(middleware.AuthMiddleware(f.oauth)) 37 + r.Post("/begin", f.BeginFocus) 38 + r.Post("/end", f.EndFocus) 39 + r.Post("/next", f.FocusNext) 40 + return r 41 + } 42 + 43 + // BeginFocus activates focus mode and redirects the user to the oldest unread 44 + // focus-eligible notification. 45 + func (f *Focus) BeginFocus(w http.ResponseWriter, r *http.Request) { 46 + l := f.logger.With("handler", "BeginFocus") 47 + did := f.oauth.GetDid(r) 48 + 49 + if err := db.BeginFocus(f.db, did); err != nil { 50 + l.Error("failed to begin focus", "err", err) 51 + http.Error(w, "internal error", http.StatusInternalServerError) 52 + return 53 + } 54 + 55 + item, err := db.GetNextFocusItem(f.db, did) 56 + if err != nil { 57 + l.Error("failed to get first focus item", "err", err) 58 + _ = db.EndFocus(f.db, did) 59 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 60 + return 61 + } 62 + if item == nil { 63 + _ = db.EndFocus(f.db, did) 64 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 65 + return 66 + } 67 + 68 + target := item.URL(f.resolver) 69 + if target == "" { 70 + _ = db.EndFocus(f.db, did) 71 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 72 + return 73 + } 74 + 75 + http.Redirect(w, r, target, http.StatusSeeOther) 76 + } 77 + 78 + // EndFocus deactivates focus mode and redirects to /notifications. 79 + func (f *Focus) EndFocus(w http.ResponseWriter, r *http.Request) { 80 + l := f.logger.With("handler", "EndFocus") 81 + did := f.oauth.GetDid(r) 82 + 83 + if err := db.EndFocus(f.db, did); err != nil { 84 + l.Error("failed to end focus", "err", err) 85 + http.Error(w, "internal error", http.StatusInternalServerError) 86 + return 87 + } 88 + 89 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 90 + } 91 + 92 + // FocusNext marks the current focus notification as read and redirects to the 93 + // next focus item. If the queue is empty, focus mode is ended. 94 + func (f *Focus) FocusNext(w http.ResponseWriter, r *http.Request) { 95 + l := f.logger.With("handler", "FocusNext") 96 + did := f.oauth.GetDid(r) 97 + 98 + // Mark the current item read so it leaves the queue. 99 + if currentIDStr := r.FormValue("current_id"); currentIDStr != "" { 100 + if currentID, err := strconv.ParseInt(currentIDStr, 10, 64); err == nil && currentID > 0 { 101 + if err := db.MarkNotificationRead(f.db, currentID, did); err != nil { 102 + l.Warn("failed to mark notification read", "id", currentID, "err", err) 103 + } 104 + } 105 + } 106 + 107 + item, err := db.GetNextFocusItem(f.db, did) 108 + if err != nil { 109 + l.Error("failed to get next focus item", "err", err) 110 + http.Error(w, "internal error", http.StatusInternalServerError) 111 + return 112 + } 113 + if item == nil { 114 + _ = db.EndFocus(f.db, did) 115 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 116 + return 117 + } 118 + 119 + target := item.URL(f.resolver) 120 + if target == "" { 121 + _ = db.EndFocus(f.db, did) 122 + http.Redirect(w, r, "/notifications", http.StatusSeeOther) 123 + return 124 + } 125 + 126 + http.Redirect(w, r, target, http.StatusSeeOther) 127 + }