From f68d72ae0eb2eb6c15cd225c1a3b9185aaa20e3f Mon Sep 17 00:00:00 2001 From: r Date: Sun, 15 Dec 2019 17:37:58 +0000 Subject: [PATCH] Add notification support --- mastodon/notification.go | 24 ++++++++-- renderer/model.go | 54 ++++++++++++++++----- renderer/renderer.go | 5 ++ service/auth.go | 8 ++++ service/logging.go | 8 ++++ service/service.go | 96 ++++++++++++++++++++++++++++++++++--- service/transport.go | 13 +++++ static/main.css | 24 ++++++++-- templates/navigation.tmpl | 1 + templates/notification.tmpl | 59 +++++++++++++++++++++++ templates/status.tmpl | 4 ++ templates/thread.tmpl | 2 +- templates/timeline.tmpl | 2 +- 13 files changed, 271 insertions(+), 29 deletions(-) create mode 100644 templates/notification.tmpl diff --git a/mastodon/notification.go b/mastodon/notification.go index 236fcbf..d793905 100644 --- a/mastodon/notification.go +++ b/mastodon/notification.go @@ -4,16 +4,22 @@ import ( "context" "fmt" "net/http" + "net/url" "time" ) +type NotificationPleroma struct { + IsSeen bool `json:"is_seen"` +} + // Notification hold information for mastodon notification. type Notification struct { - ID string `json:"id"` - Type string `json:"type"` - CreatedAt time.Time `json:"created_at"` - Account Account `json:"account"` - Status *Status `json:"status"` + ID string `json:"id"` + Type string `json:"type"` + CreatedAt time.Time `json:"created_at"` + Account Account `json:"account"` + Status *Status `json:"status"` + Pleroma *NotificationPleroma `json:"pleroma"` } // GetNotifications return notifications. @@ -40,3 +46,11 @@ func (c *Client) GetNotification(ctx context.Context, id string) (*Notification, func (c *Client) ClearNotifications(ctx context.Context) error { return c.doAPI(ctx, http.MethodPost, "/api/v1/notifications/clear", nil, nil, nil) } + +// ReadNotifications marks notifications as read +// Currenly only works for Pleroma +func (c *Client) ReadNotifications(ctx context.Context, maxID string) error { + params := url.Values{} + params.Set("max_id", maxID) + return c.doAPI(ctx, http.MethodPost, "/api/v1/pleroma/notifications/read", params, nil, nil) +} diff --git a/renderer/model.go b/renderer/model.go index 6f6acc4..4529386 100644 --- a/renderer/model.go +++ b/renderer/model.go @@ -4,22 +4,34 @@ import ( "mastodon" ) +type NavbarTemplateData struct { + NotificationCount int +} + +func NewNavbarTemplateData(notificationCount int) *NavbarTemplateData { + return &NavbarTemplateData{ + NotificationCount: notificationCount, + } +} + type TimelinePageTemplateData struct { - Statuses []*mastodon.Status - HasNext bool - NextLink string - HasPrev bool - PrevLink string + Statuses []*mastodon.Status + HasNext bool + NextLink string + HasPrev bool + PrevLink string + NavbarData *NavbarTemplateData } func NewTimelinePageTemplateData(statuses []*mastodon.Status, hasNext bool, nextLink string, hasPrev bool, - prevLink string) *TimelinePageTemplateData { + prevLink string, navbarData *NavbarTemplateData) *TimelinePageTemplateData { return &TimelinePageTemplateData{ - Statuses: statuses, - HasNext: hasNext, - NextLink: nextLink, - HasPrev: hasPrev, - PrevLink: prevLink, + Statuses: statuses, + HasNext: hasNext, + NextLink: nextLink, + HasPrev: hasPrev, + PrevLink: prevLink, + NavbarData: navbarData, } } @@ -29,14 +41,32 @@ type ThreadPageTemplateData struct { PostReply bool ReplyToID string ReplyContent string + NavbarData *NavbarTemplateData } -func NewThreadPageTemplateData(status *mastodon.Status, context *mastodon.Context, postReply bool, replyToID string, replyContent string) *ThreadPageTemplateData { +func NewThreadPageTemplateData(status *mastodon.Status, context *mastodon.Context, postReply bool, replyToID string, replyContent string, navbarData *NavbarTemplateData) *ThreadPageTemplateData { return &ThreadPageTemplateData{ Status: status, Context: context, PostReply: postReply, ReplyToID: replyToID, ReplyContent: replyContent, + NavbarData: navbarData, + } +} + +type NotificationPageTemplateData struct { + Notifications []*mastodon.Notification + HasNext bool + NextLink string + NavbarData *NavbarTemplateData +} + +func NewNotificationPageTemplateData(notifications []*mastodon.Notification, hasNext bool, nextLink string, navbarData *NavbarTemplateData) *NotificationPageTemplateData { + return &NotificationPageTemplateData{ + Notifications: notifications, + HasNext: hasNext, + NextLink: nextLink, + NavbarData: navbarData, } } diff --git a/renderer/renderer.go b/renderer/renderer.go index c3d3526..394d74f 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -17,6 +17,7 @@ type Renderer interface { RenderSigninPage(ctx context.Context, writer io.Writer) (err error) RenderTimelinePage(ctx context.Context, writer io.Writer, data *TimelinePageTemplateData) (err error) RenderThreadPage(ctx context.Context, writer io.Writer, data *ThreadPageTemplateData) (err error) + RenderNotificationPage(ctx context.Context, writer io.Writer, data *NotificationPageTemplateData) (err error) } type renderer struct { @@ -60,6 +61,10 @@ func (r *renderer) RenderThreadPage(ctx context.Context, writer io.Writer, data return r.template.ExecuteTemplate(writer, "thread.tmpl", data) } +func (r *renderer) RenderNotificationPage(ctx context.Context, writer io.Writer, data *NotificationPageTemplateData) (err error) { + return r.template.ExecuteTemplate(writer, "notification.tmpl", data) +} + func WithEmojis(content string, emojis []mastodon.Emoji) string { var emojiNameContentPair []string for _, e := range emojis { diff --git a/service/auth.go b/service/auth.go index 98012af..e9bec38 100644 --- a/service/auth.go +++ b/service/auth.go @@ -111,6 +111,14 @@ func (s *authService) ServeThreadPage(ctx context.Context, client io.Writer, c * return s.Service.ServeThreadPage(ctx, client, c, id, reply) } +func (s *authService) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) { + c, err = s.getClient(ctx) + if err != nil { + return + } + return s.Service.ServeNotificationPage(ctx, client, c, maxID, minID) +} + func (s *authService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { c, err = s.getClient(ctx) if err != nil { diff --git a/service/logging.go b/service/logging.go index 3a95a94..aa1da68 100644 --- a/service/logging.go +++ b/service/logging.go @@ -77,6 +77,14 @@ func (s *loggingService) ServeThreadPage(ctx context.Context, client io.Writer, return s.Service.ServeThreadPage(ctx, client, c, id, reply) } +func (s *loggingService) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) { + defer func(begin time.Time) { + s.logger.Printf("method=%v, max_id=%v, min_id=%v, took=%v, err=%v\n", + "ServeNotificationPage", maxID, minID, time.Since(begin), err) + }(time.Now()) + return s.Service.ServeNotificationPage(ctx, client, c, maxID, minID) +} + func (s *loggingService) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { defer func(begin time.Time) { s.logger.Printf("method=%v, id=%v, took=%v, err=%v\n", diff --git a/service/service.go b/service/service.go index e502b65..93f22fa 100644 --- a/service/service.go +++ b/service/service.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "errors" - "fmt" "io" "mime/multipart" "net/http" @@ -33,6 +32,7 @@ type Service interface { ServeSigninPage(ctx context.Context, client io.Writer) (err error) ServeTimelinePage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, sinceID string, minID string) (err error) ServeThreadPage(ctx context.Context, client io.Writer, c *mastodon.Client, id string, reply bool) (err error) + ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) UnLike(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) Retweet(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) @@ -219,7 +219,7 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, if len(maxID) > 0 && len(statuses) > 0 { hasPrev = true - prevLink = fmt.Sprintf("/timeline?min_id=%s", statuses[0].ID) + prevLink = "/timeline?min_id=" + statuses[0].ID } if len(minID) > 0 && len(pg.MinID) > 0 { newStatuses, err := c.GetTimelineHome(ctx, &mastodon.Pagination{MinID: pg.MinID, Limit: 20}) @@ -229,21 +229,26 @@ func (svc *service) ServeTimelinePage(ctx context.Context, client io.Writer, newStatusesLen := len(newStatuses) if newStatusesLen == 20 { hasPrev = true - prevLink = fmt.Sprintf("/timeline?min_id=%s", pg.MinID) + prevLink = "/timeline?min_id=" + pg.MinID } else { i := 20 - newStatusesLen - 1 if len(statuses) > i { hasPrev = true - prevLink = fmt.Sprintf("/timeline?min_id=%s", statuses[i].ID) + prevLink = "/timeline?min_id=" + statuses[i].ID } } } if len(pg.MaxID) > 0 { hasNext = true - nextLink = fmt.Sprintf("/timeline?max_id=%s", pg.MaxID) + nextLink = "/timeline?max_id=" + pg.MaxID } - data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink) + navbarData, err := svc.getNavbarTemplateData(ctx, client, c) + if err != nil { + return + } + + data := renderer.NewTimelinePageTemplateData(statuses, hasNext, nextLink, hasPrev, prevLink, navbarData) err = svc.renderer.RenderTimelinePage(ctx, client, data) if err != nil { return @@ -280,7 +285,12 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *ma } } - data := renderer.NewThreadPageTemplateData(status, context, reply, id, content) + navbarData, err := svc.getNavbarTemplateData(ctx, client, c) + if err != nil { + return + } + + data := renderer.NewThreadPageTemplateData(status, context, reply, id, content, navbarData) err = svc.renderer.RenderThreadPage(ctx, client, data) if err != nil { return @@ -289,6 +299,78 @@ func (svc *service) ServeThreadPage(ctx context.Context, client io.Writer, c *ma return } +func (svc *service) ServeNotificationPage(ctx context.Context, client io.Writer, c *mastodon.Client, maxID string, minID string) (err error) { + var hasNext bool + var nextLink string + + var pg = mastodon.Pagination{ + MaxID: maxID, + MinID: minID, + Limit: 20, + } + + notifications, err := c.GetNotifications(ctx, &pg) + if err != nil { + return + } + + var unreadCount int + for i := range notifications { + switch notifications[i].Type { + case "reblog", "favourite": + if notifications[i].Status != nil { + notifications[i].Status.Account.ID = "" + } + } + if notifications[i].Pleroma != nil && notifications[i].Pleroma.IsSeen { + unreadCount++ + } + } + + if unreadCount > 0 { + err := c.ReadNotifications(ctx, notifications[0].ID) + if err != nil { + return err + } + } + + if len(pg.MaxID) > 0 { + hasNext = true + nextLink = "/notifications?max_id=" + pg.MaxID + } + + navbarData, err := svc.getNavbarTemplateData(ctx, client, c) + if err != nil { + return + } + + data := renderer.NewNotificationPageTemplateData(notifications, hasNext, nextLink, navbarData) + err = svc.renderer.RenderNotificationPage(ctx, client, data) + if err != nil { + return + } + + return +} + +func (svc *service) getNavbarTemplateData(ctx context.Context, client io.Writer, c *mastodon.Client) (data *renderer.NavbarTemplateData, err error) { + notifications, err := c.GetNotifications(ctx, nil) + if err != nil { + return + } + + var notificationCount int + for i := range notifications { + if notifications[i].Pleroma != nil && !notifications[i].Pleroma.IsSeen { + notificationCount++ + } + } + + data = renderer.NewNavbarTemplateData(notificationCount) + + return +} + func (svc *service) Like(ctx context.Context, client io.Writer, c *mastodon.Client, id string) (err error) { _, err = c.Favourite(ctx, id) return diff --git a/service/transport.go b/service/transport.go index d5a6ee8..377ab23 100644 --- a/service/transport.go +++ b/service/transport.go @@ -179,6 +179,19 @@ func NewHandler(s Service, staticDir string) http.Handler { w.WriteHeader(http.StatusSeeOther) }).Methods(http.MethodPost) + r.HandleFunc("/notifications", func(w http.ResponseWriter, req *http.Request) { + ctx := getContextWithSession(context.Background(), req) + + maxID := req.URL.Query().Get("max_id") + minID := req.URL.Query().Get("min_id") + + err := s.ServeNotificationPage(ctx, w, nil, maxID, minID) + if err != nil { + s.ServeErrorPage(ctx, w, err) + return + } + }).Methods(http.MethodGet) + r.HandleFunc("/signout", func(w http.ResponseWriter, req *http.Request) { // TODO remove session from database w.Header().Add("Set-Cookie", fmt.Sprintf("session_id=;max-age=0")) diff --git a/static/main.css b/static/main.css index 3f551cc..8865820 100644 --- a/static/main.css +++ b/static/main.css @@ -17,11 +17,10 @@ .status-profile-img { height: 48px; width: 48px; - object-fit: contain; + margin-right: 8px; } .status { - margin: 0 8px; } .status a { @@ -119,7 +118,6 @@ height: 24px; width: 24px; margin-bottom: -8px; - object-fit: contain; } .retweet-info .status-dname{ @@ -143,3 +141,23 @@ .pagination a { margin: 0 8px; } + +.notification-container { + margin: 4px 0; + padding: 4px 4px; + border-left: 4px solid transparent; +} + +.notification-container.unread { + border-color: #777777; +} + +.notification-follow-container, +.notification-like-container, +.notification-retweet-container { + display: flex; +} + +.notification-follow-uname { + margin-top: 8px; +} diff --git a/templates/navigation.tmpl b/templates/navigation.tmpl index ea4a213..d86971c 100644 --- a/templates/navigation.tmpl +++ b/templates/navigation.tmpl @@ -1,4 +1,5 @@ diff --git a/templates/notification.tmpl b/templates/notification.tmpl new file mode 100644 index 0000000..099f17e --- /dev/null +++ b/templates/notification.tmpl @@ -0,0 +1,59 @@ +{{template "header.tmpl"}} +{{template "navigation.tmpl" .NavbarData}} +
Notifications
+ +{{range .Notifications}} +
+ {{if eq .Type "follow"}} +
+ profile-avatar +
+
+ {{WithEmojis .Account.DisplayName .Account.Emojis}} + + followed you +
+
+ @{{.Account.Acct}} +
+
+
+ + {{else if eq .Type "mention"}} + {{template "status" .Status}} + + {{else if eq .Type "reblog"}} +
+ profile-avatar +
+
+ {{WithEmojis .Account.DisplayName .Account.Emojis}} + + retweeted your post +
+ {{template "status" .Status}} +
+
+ + {{else if eq .Type "favourite"}} +
+ profile-avatar +
+
+ {{WithEmojis .Account.DisplayName .Account.Emojis}} + + liked your post +
+ {{template "status" .Status}} +
+
+ {{end}} +
+{{end}} + + +{{template "footer.tmpl"}} diff --git a/templates/status.tmpl b/templates/status.tmpl index 24f9a54..7020be0 100644 --- a/templates/status.tmpl +++ b/templates/status.tmpl @@ -11,13 +11,17 @@ {{block "status" .}}
+ {{if ne .Account.ID ""}} profile-avatar + {{end}}
+ {{if ne .Account.ID ""}}
{{WithEmojis .Account.DisplayName .Account.Emojis}} {{.Account.Acct}}
+ {{end}}
{{WithEmojis .Content .Emojis}}
{{range .MediaAttachments}} diff --git a/templates/thread.tmpl b/templates/thread.tmpl index a3f7916..29d702b 100644 --- a/templates/thread.tmpl +++ b/templates/thread.tmpl @@ -1,6 +1,6 @@ {{template "header.tmpl"}} +{{template "navigation.tmpl" .NavbarData}}
Thread
-{{template "navigation.tmpl"}} {{range .Context.Ancestors}} {{template "status.tmpl" .}} diff --git a/templates/timeline.tmpl b/templates/timeline.tmpl index 527c91b..53e3ad7 100644 --- a/templates/timeline.tmpl +++ b/templates/timeline.tmpl @@ -1,6 +1,6 @@ {{template "header.tmpl"}} +{{template "navigation.tmpl" .NavbarData}}
Timeline
-{{template "navigation.tmpl"}}