Monorepo for Tangled
tangled.org
1package focus
2
3import (
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
16type Focus struct {
17 db *db.DB
18 oauth *oauth.OAuth
19 pages *pages.Pages
20 logger *slog.Logger
21 resolver *idresolver.Resolver
22}
23
24func 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
34func (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.
45func (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.
79func (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.
94func (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}