Add bookmarks
authorr <r@freesoftwareextremist.com>
Sun, 27 Sep 2020 09:29:17 +0000 (09:29 +0000)
committerr <r@freesoftwareextremist.com>
Sun, 27 Sep 2020 09:37:15 +0000 (09:37 +0000)
- Add bookmark/unbookmark link on mouse hover
- Add bookmarks section on user profile page

mastodon/accounts.go
mastodon/status.go
service/auth.go
service/logging.go
service/service.go
service/transport.go
templates/status.tmpl
templates/user.tmpl

index 7a44e2b0e51bc891d2172353eaf54109ffb7e0ca..c5eb227335cca84828ab635e34abf7bdba184edd 100644 (file)
@@ -353,3 +353,13 @@ func (c *Client) UnSubscribe(ctx context.Context, id string) (*Relationship, err
        }
        return relationship, nil
 }
+
+// GetBookmarks returns the list of bookmarked statuses
+func (c *Client) GetBookmarks(ctx context.Context, pg *Pagination) ([]*Status, error) {
+       var statuses []*Status
+       err := c.doAPI(ctx, http.MethodGet, "/api/v1/bookmarks", nil, &statuses, pg)
+       if err != nil {
+               return nil, err
+       }
+       return statuses, nil
+}
index 7a29b99e3dbb92f6d3d82c1ebc6f03a2bebff3ce..c8555d6dd116754a9fea6e0a3831382c246b3080 100644 (file)
@@ -47,6 +47,7 @@ type Status struct {
        Application        Application  `json:"application"`
        Language           string       `json:"language"`
        Pinned             interface{}  `json:"pinned"`
+       Bookmarked         bool         `json:"bookmarked"`
        Poll               *Poll        `json:"poll"`
 
        // Custom fields
@@ -366,3 +367,25 @@ func (c *Client) UnmuteConversation(ctx context.Context, id string) (*Status, er
        }
        return &status, nil
 }
+
+// Bookmark bookmarks status specified by id.
+func (c *Client) Bookmark(ctx context.Context, id string) (*Status, error) {
+       var status Status
+
+       err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/bookmark", id), nil, &status, nil)
+       if err != nil {
+               return nil, err
+       }
+       return &status, nil
+}
+
+// Unbookmark bookmarks status specified by id.
+func (c *Client) Unbookmark(ctx context.Context, id string) (*Status, error) {
+       var status Status
+
+       err := c.doAPI(ctx, http.MethodPost, fmt.Sprintf("/api/v1/statuses/%s/unbookmark", id), nil, &status, nil)
+       if err != nil {
+               return nil, err
+       }
+       return &status, nil
+}
index ef701c1cd7f652accfeb27462220470e5c6998bb..7845675058332aa4c3380a6a46e909db7bb0c31c 100644 (file)
@@ -443,8 +443,7 @@ func (s *as) Delete(c *model.Client, id string) (err error) {
        return s.Service.Delete(c, id)
 }
 
-func (s *as) ReadNotifications(c *model.Client,
-       maxID string) (err error) {
+func (s *as) ReadNotifications(c *model.Client, maxID string) (err error) {
        err = s.authenticateClient(c)
        if err != nil {
                return
@@ -455,3 +454,27 @@ func (s *as) ReadNotifications(c *model.Client,
        }
        return s.Service.ReadNotifications(c, maxID)
 }
+
+func (s *as) Bookmark(c *model.Client, id string) (err error) {
+       err = s.authenticateClient(c)
+       if err != nil {
+               return
+       }
+       err = checkCSRF(c)
+       if err != nil {
+               return
+       }
+       return s.Service.Bookmark(c, id)
+}
+
+func (s *as) UnBookmark(c *model.Client, id string) (err error) {
+       err = s.authenticateClient(c)
+       if err != nil {
+               return
+       }
+       err = checkCSRF(c)
+       if err != nil {
+               return
+       }
+       return s.Service.UnBookmark(c, id)
+}
index 7df03def2dcc99aa8ea737184a282b7db7742daa..3cb99bf6fdb282c1ab5e87fee8cbdbca4bfae8c1 100644 (file)
@@ -316,11 +316,26 @@ func (s *ls) Delete(c *model.Client, id string) (err error) {
        return s.Service.Delete(c, id)
 }
 
-func (s *ls) ReadNotifications(c *model.Client,
-       maxID string) (err error) {
+func (s *ls) ReadNotifications(c *model.Client, maxID string) (err error) {
        defer func(begin time.Time) {
                s.logger.Printf("method=%v, max_id=%v, took=%v, err=%v\n",
                        "ReadNotifications", maxID, time.Since(begin), err)
        }(time.Now())
        return s.Service.ReadNotifications(c, maxID)
 }
+
+func (s *ls) Bookmark(c *model.Client, id string) (err error) {
+       defer func(begin time.Time) {
+               s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
+                       "Bookmark", id, time.Since(begin), err)
+       }(time.Now())
+       return s.Service.Bookmark(c, id)
+}
+
+func (s *ls) UnBookmark(c *model.Client, id string) (err error) {
+       defer func(begin time.Time) {
+               s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n",
+                       "UnBookmark", id, time.Since(begin), err)
+       }(time.Now())
+       return s.Service.UnBookmark(c, id)
+}
index c56d96ac4245184e96fb64931c58a305acf68305..de450b94a1685653e7d7502e8f0f9269ac3fcdc8 100644 (file)
@@ -62,6 +62,8 @@ type Service interface {
        UnMuteConversation(c *model.Client, id string) (err error)
        Delete(c *model.Client, id string) (err error)
        ReadNotifications(c *model.Client, maxID string) (err error)
+       Bookmark(c *model.Client, id string) (err error)
+       UnBookmark(c *model.Client, id string) (err error)
 }
 
 type service struct {
@@ -109,13 +111,13 @@ func getRendererContext(c *model.Client) *renderer.Context {
                settings = *model.NewSettings()
        }
        return &renderer.Context{
-               HideAttachments: settings.HideAttachments,
-               MaskNSFW:        settings.MaskNSFW,
-               ThreadInNewTab:  settings.ThreadInNewTab,
-               FluorideMode:    settings.FluorideMode,
-               DarkMode:        settings.DarkMode,
-               CSRFToken:       session.CSRFToken,
-               UserID:          session.UserID,
+               HideAttachments:  settings.HideAttachments,
+               MaskNSFW:         settings.MaskNSFW,
+               ThreadInNewTab:   settings.ThreadInNewTab,
+               FluorideMode:     settings.FluorideMode,
+               DarkMode:         settings.DarkMode,
+               CSRFToken:        session.CSRFToken,
+               UserID:           session.UserID,
                AntiDopamineMode: settings.AntiDopamineMode,
        }
 }
@@ -464,6 +466,7 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,
        if err != nil {
                return
        }
+       isCurrent := c.Session.UserID == user.ID
 
        switch pageType {
        case "":
@@ -502,6 +505,18 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,
                        nextLink = fmt.Sprintf("/user/%s/media?max_id=%s",
                                id, pg.MaxID)
                }
+       case "bookmarks":
+               if !isCurrent {
+                       return errInvalidArgument
+               }
+               statuses, err = c.GetBookmarks(ctx, &pg)
+               if err != nil {
+                       return
+               }
+               if len(statuses) == 20 && len(pg.MaxID) > 0 {
+                       nextLink = fmt.Sprintf("/user/%s/bookmarks?max_id=%s",
+                               id, pg.MaxID)
+               }
        default:
                return errInvalidArgument
        }
@@ -509,7 +524,7 @@ func (svc *service) ServeUserPage(c *model.Client, id string, pageType string,
        commonData := svc.getCommonData(c, user.DisplayName)
        data := &renderer.UserData{
                User:       user,
-               IsCurrent:  c.Session.UserID == user.ID,
+               IsCurrent:  isCurrent,
                Type:       pageType,
                Users:      users,
                Statuses:   statuses,
@@ -890,3 +905,13 @@ func (svc *service) Delete(c *model.Client, id string) (err error) {
 func (svc *service) ReadNotifications(c *model.Client, maxID string) (err error) {
        return c.ReadNotifications(ctx, maxID)
 }
+
+func (svc *service) Bookmark(c *model.Client, id string) (err error) {
+       _, err = c.Bookmark(ctx, id)
+       return
+}
+
+func (svc *service) UnBookmark(c *model.Client, id string) (err error) {
+       _, err = c.Unbookmark(ctx, id)
+       return
+}
index 7d27a848e02207eb8998c66c61a2221f0afee301..4f73c5ea9edfcd25748bec6e1485c528a10b796f 100644 (file)
@@ -76,7 +76,7 @@ func NewHandler(s Service, staticDir string) http.Handler {
                        c := newClient(w, req, "")
                        err := s.ServeRootPage(c)
                        if err != nil {
-                               if (err == errInvalidAccessToken) {
+                               if err == errInvalidAccessToken {
                                        w.Header().Add("Location", "/signin")
                                        w.WriteHeader(http.StatusFound)
                                        return
@@ -676,6 +676,46 @@ func NewHandler(s Service, staticDir string) http.Handler {
                w.WriteHeader(http.StatusFound)
        }
 
+       bookmark := func(w http.ResponseWriter, req *http.Request) {
+               c := newClient(w, req, req.FormValue("csrf_token"))
+               id, _ := mux.Vars(req)["id"]
+               retweetedByID := req.FormValue("retweeted_by_id")
+
+               err := s.Bookmark(c, id)
+               if err != nil {
+                       w.WriteHeader(http.StatusInternalServerError)
+                       s.ServeErrorPage(c, err)
+                       return
+               }
+
+               rID := id
+               if len(retweetedByID) > 0 {
+                       rID = retweetedByID
+               }
+               w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
+               w.WriteHeader(http.StatusFound)
+       }
+
+       unBookmark := func(w http.ResponseWriter, req *http.Request) {
+               c := newClient(w, req, req.FormValue("csrf_token"))
+               id, _ := mux.Vars(req)["id"]
+               retweetedByID := req.FormValue("retweeted_by_id")
+
+               err := s.UnBookmark(c, id)
+               if err != nil {
+                       w.WriteHeader(http.StatusInternalServerError)
+                       s.ServeErrorPage(c, err)
+                       return
+               }
+
+               rID := id
+               if len(retweetedByID) > 0 {
+                       rID = retweetedByID
+               }
+               w.Header().Add("Location", req.Header.Get("Referer")+"#status-"+rID)
+               w.WriteHeader(http.StatusFound)
+       }
+
        signout := func(w http.ResponseWriter, req *http.Request) {
                c := newClient(w, req, req.FormValue("csrf_token"))
 
@@ -791,6 +831,8 @@ func NewHandler(s Service, staticDir string) http.Handler {
        r.HandleFunc("/unmuteconv/{id}", unMuteConversation).Methods(http.MethodPost)
        r.HandleFunc("/delete/{id}", delete).Methods(http.MethodPost)
        r.HandleFunc("/notifications/read", readNotifications).Methods(http.MethodPost)
+       r.HandleFunc("/bookmark/{id}", bookmark).Methods(http.MethodPost)
+       r.HandleFunc("/unbookmark/{id}", unBookmark).Methods(http.MethodPost)
        r.HandleFunc("/signout", signout).Methods(http.MethodPost)
        r.HandleFunc("/fluoride/like/{id}", fLike).Methods(http.MethodPost)
        r.HandleFunc("/fluoride/unlike/{id}", fUnlike).Methods(http.MethodPost)
index 1e3d5149c372a221b1ed11aacc588ff872555c99..6c255a07879ebe746f0d2cce1bc4f4fc00e6e96a 100644 (file)
                                                        <input type="submit" value="mute" class="btn-link more-link">
                                                </form>
                                                {{end}}
+                                               {{if .Bookmarked}}
+                                               <form action="/unbookmark/{{.ID}}" method="post" target="_self">
+                                                       <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+                                                       <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">
+                                                       <input type="submit" value="unbookmark" class="btn-link more-link">
+                                               </form>
+                                               {{else}}
+                                               <form action="/bookmark/{{.ID}}" method="post" target="_self">
+                                                       <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
+                                                       <input type="hidden" name="retweeted_by_id" value="{{.RetweetedByID}}">
+                                                       <input type="submit" value="bookmark" class="btn-link more-link">
+                                               </form>
+                                               {{end}}
                                                {{if eq $.Ctx.UserID .Account.ID}}
                                                <form action="/delete/{{.ID}}" method="post" target="_self">
                                                        <input type="hidden" name="csrf_token" value="{{$.Ctx.CSRFToken}}">
index cb21b8a3709985d1a50b35a3e0cf7614315aba3d..3bb7523781236ac814fd5366e631129e1f6c6034 100644 (file)
                        <a href="/user/{{.User.ID}}/followers"> followers ({{.User.FollowersCount}}) </a> - 
                        <a href="/user/{{.User.ID}}/media"> media </a>
                </div>
+               {{if .IsCurrent}}
+               <div>
+                       <a href="/user/{{.User.ID}}/bookmarks"> bookmarks </a>
+               </div>
+               {{end}}
                <div>
                        <a href="/usersearch/{{.User.ID}}"> search statuses </a>
                </div>
 <div class="page-title"> Statuses </div>
 {{range .Statuses}}
 {{template "status.tmpl" (WithContext . $.Ctx)}}
+{{else}}
+<div class="no-data-found">No data found</div>
 {{end}}
 
 {{else if eq .Type "following"}}
 <div class="page-title"> Statuses with media </div>
 {{range .Statuses}}
 {{template "status.tmpl" (WithContext . $.Ctx)}}
+{{else}}
+<div class="no-data-found">No data found</div>
+{{end}}
+
+{{else if eq .Type "bookmarks"}}
+<div class="page-title"> Bookmarks </div>
+{{range .Statuses}}
+{{template "status.tmpl" (WithContext . $.Ctx)}}
+{{else}}
+<div class="no-data-found">No data found</div>
 {{end}}
 {{end}}