Added support for liking notes in the ui. Also implements undo/like activity in user inbox.

This commit is contained in:
Nick Gerakines 2020-04-25 18:41:44 -04:00
parent 4657c9415b
commit c808cf236b
No known key found for this signature in database
GPG Key ID: 33D43D854F96B2E4
8 changed files with 214 additions and 4 deletions

View File

@ -53,7 +53,7 @@ type ObjectStorage interface {
CountObjectBoostsByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
CountObjectBoostsByActorObjectIDs(ctx context.Context, actorRowID uuid.UUID, objectRowIDs []uuid.UUID) ([]Count, error)
CountObjectRepliesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
CountObjectLikesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error)
ExistsObjectInUserFeedByObjectID(ctx context.Context, objectID string) (bool, error)
ObjectParentsByObjectID(ctx context.Context, objectRowID uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
ObjectChildrenByObjectID(ctx context.Context, objectRowID uuid.UUID) (map[uuid.UUID][]uuid.UUID, error)
@ -306,6 +306,15 @@ func (s pgStorage) CountObjectBoostsByObjectIDs(ctx context.Context, objectIDs [
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(objectIDs)...)
}
func (s pgStorage) CountObjectLikesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error) {
if len(objectIDs) == 0 {
return []Count{}, nil
}
placeholders := common.DollarForEach(len(objectIDs))
query := fmt.Sprintf(`SELECT object_id, COUNT(*) FROM object_likes WHERE object_id IN (%s) GROUP BY object_id`, strings.Join(placeholders, ","))
return s.keyedCount(errors.WrapObjectBoostQueryFailedError, ctx, query, common.UUIDsToInterfaces(objectIDs)...)
}
func (s pgStorage) CountObjectRepliesByObjectIDs(ctx context.Context, objectIDs []uuid.UUID) ([]Count, error) {
if len(objectIDs) == 0 {
return []Count{}, nil

View File

@ -14,6 +14,19 @@
}));
newForm.hide().appendTo("body").submit();
});
$('body').on('click', 'a.like', function () {
let newForm = jQuery('<form>', {
'action': '/dashboard/notes/like/note',
'method': 'post',
'target': '_top'
}).append(jQuery('<input>', {
'name': 'object',
'value': $(this).data('object'),
'type': 'hidden'
}));
newForm.hide().appendTo("body").submit();
});
});
</script>

View File

@ -35,6 +35,13 @@
Announce
</a>
{{ end }}
{{ if .ViewContext.LinkLike }}
<a href="#" class="dropdown-item like"
data-object="{{ .ViewContext.ObjectID }}">
<i class="far fa-thumbs-up"></i>
Like
</a>
{{ end }}
{{ if and (or .ViewContext.LinkView .ViewContext.LinkReply) .ViewContext.LinkDelete }}
{{ end }}
{{ if .ViewContext.LinkDelete }}
@ -104,7 +111,7 @@
{{ end }}
</ul>
{{ end }}
{{ if or .ViewContext.TotalAnnounces .ViewContext.TotalReplies .ViewContext.ViewerAnnounced }}
{{ if or .ViewContext.TotalAnnounces .ViewContext.TotalReplies .ViewContext.TotalLikes .ViewContext.ViewerAnnounced }}
<ul class="list-inline text-muted">
{{ if .ViewContext.TotalAnnounces }}
<li class="list-inline-item">
@ -117,6 +124,9 @@
{{ if .ViewContext.TotalReplies }}
<li class="list-inline-item">{{ .ViewContext.TotalReplies }} replies</li>
{{ end }}
{{ if .ViewContext.TotalLikes }}
<li class="list-inline-item">{{ .ViewContext.TotalLikes }} likes</li>
{{ end }}
</ul>
{{ end }}

View File

@ -406,6 +406,7 @@ func serverCommandAction(cliCtx *cli.Context) error {
authenticated.GET("/compose", h.compose)
authenticated.POST("/compose/create/note", h.createNote)
authenticated.POST("/dashboard/notes/announce/note", h.announceNote)
authenticated.POST("/dashboard/notes/like/note", h.likeNote)
authenticated.POST("/notes/delete", h.deleteNote)
authenticated.GET("/configure", h.configure)

View File

@ -613,6 +613,163 @@ func (h handler) announceNote(c *gin.Context) {
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func (h handler) likeNote(c *gin.Context) {
session := sessions.Default(c)
ctx := c.Request.Context()
user, err := h.storage.GetUserBySession(ctx, session)
if err != nil {
if errors.Is(err, errors.UserSessionNotFoundError{}) {
if err = appendFlashError(session, "Not Authenticated"); err != nil {
h.hardFail(c, errors.NewCannotSaveSessionError(err))
return
}
c.Redirect(http.StatusFound, h.url())
return
}
h.hardFail(c, err)
return
}
userActor, err := h.storage.GetActor(ctx, user.ActorID)
if err != nil {
h.hardFail(c, err)
return
}
userActorFollowers, _ := storage.JSONString(userActor.Payload, "followers")
objectID := c.PostForm("object")
if len(objectID) == 0 {
h.flashErrorOrFail(c, h.url("feed_recent"), fmt.Errorf("invalid object id"))
return
}
objectPayload, err := h.storage.ObjectPayloadByObjectID(ctx, objectID)
if err != nil {
h.hardFail(c, err)
return
}
attributedTo, hasAttributedTo := storage.JSONString(objectPayload, "attributedTo")
if !hasAttributedTo {
h.hardFail(c, fmt.Errorf("object has no attributedTo attribute"), zap.String("object_id", objectID))
return
}
if userActor.ActorID == attributedTo {
h.hardFail(c, fmt.Errorf("you cannot like your own stuff"), zap.String("object_id", objectID))
return
}
sourceActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, attributedTo)
if err != nil {
h.hardFail(c, err, zap.String("actor", attributedTo))
return
}
sourceActorInbox, hasSourceActorInbox := storage.JSONString(sourceActor.Payload, "inbox")
if !hasSourceActorInbox {
h.hardFail(c, fmt.Errorf("actor has no inbox attribute"), zap.String("actor", attributedTo))
return
}
now := time.Now().UTC()
publishedAt := now.Format("2006-01-02T15:04:05Z")
to := []string{
"https://www.w3.org/ns/activitystreams#Public",
attributedTo,
}
cc := []string{
userActorFollowers,
}
likeURL := common.ActivityURL(h.domain, storage.NewV4())
like := storage.EmptyPayload()
like["@context"] = "https://www.w3.org/ns/activitystreams"
like["id"] = likeURL
like["type"] = "Like"
like["actor"] = userActor.ActorID
like["published"] = publishedAt
like["to"] = to
like["cc"] = cc
like["object"] = objectID
likePayload := like.Bytes()
var destinations []string
err = storage.TransactionalStorage(ctx, h.storage, func(tx storage.Storage) error {
objectRowID, err := tx.ObjectRowIDForObjectID(ctx, objectID)
if err != nil {
return err
}
activityRowID, err := tx.RecordObjectEvent(ctx, likeURL, objectRowID, like)
if err != nil {
return err
}
_, err = tx.RecordUserObjectEvent(ctx, user.ID, activityRowID, objectRowID, true)
if err != nil {
return err
}
_, err = tx.RecordLike(ctx, userActor.ID, objectRowID, like)
if err != nil {
return err
}
followerTotal, err := tx.CountFollowers(ctx, user.ID)
if err != nil {
return err
}
destinations, err = tx.ListAcceptedFollowers(ctx, user.ID, followerTotal, 0)
if err != nil {
return err
}
return nil
})
if err != nil {
h.internalServerErrorJSON(c, err)
return
}
destinations = append(destinations, sourceActorInbox)
destinations = uniqueTo(destinations, userActor.ActorID)
nc := fed.ActorClient{
HTTPClient: h.httpClient,
Logger: h.logger,
}
localActor := h.userActor(user, userActor)
for _, dest := range destinations {
foundActor, err := fed.GetOrFetchActor(ctx, h.storage, h.logger, h.httpClient, dest)
if err != nil {
h.logger.Error("unable to get or fetch actor", zap.Error(err), zap.String("actor", dest))
continue
}
if h.publisherClient != nil {
err = h.publisherClient.Send(context.Background(), foundActor.GetInbox(), userActor.GetKeyID(), user.PrivateKey, likeURL, string(likePayload))
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", likeURL), zap.Error(err))
}
} else {
err = nc.SendToInbox(ctx, localActor, foundActor, likePayload)
if err != nil {
h.logger.Error("failed sending to actor", zap.String("target", foundActor.GetID()), zap.String("activity", likeURL))
}
}
}
c.Redirect(http.StatusFound, h.url("feed_mine"))
}
func uniqueTo(input []string, exclude ...string) []string {
results := make([]string, 0)

View File

@ -152,6 +152,16 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
mappedBoosts[boost.Key] = boost.Count
}
likes, err := h.storage.CountObjectLikesByObjectIDs(ctx, collectedRowIDs)
if err != nil {
h.hardFail(c, err)
return
}
mappedLikes := make(map[string]int)
for _, like := range likes {
mappedLikes[like.Key] = like.Count
}
mappedUserBoosts := make(map[string]int)
if localUserActor != nil {
userBoosts, err := h.storage.CountObjectBoostsByActorObjectIDs(ctx, localUserActor.ID, collectedRowIDs)
@ -308,7 +318,10 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
if viewContext.LinkReply {
viewContext.LinkReplyURL = fmt.Sprintf("%s?inReplyTo=%s", h.url("compose"), url.QueryEscape(objectID))
}
viewContext.LinkAnnounce = true
if localUserActor != nil && attributedTo != localUserActor.ActorID {
viewContext.LinkAnnounce = true
viewContext.LinkLike = true
}
if localUserActor != nil && attributedTo == localUserActor.ActorID {
viewContext.LinkDelete = true
@ -397,6 +410,9 @@ func (h handler) displayObjectFeed(c *gin.Context, requireUser bool, vars map[st
if count, hasCount := mappedBoosts[viewObjectRowID.String()]; hasCount {
viewContext.TotalAnnounces = count
}
if count, hasCount := mappedLikes[viewObjectRowID.String()]; hasCount {
viewContext.TotalLikes = count
}
if count, hasCount := mappedReplies[viewObjectRowID.String()]; hasCount {
viewContext.TotalReplies = count
}

View File

@ -104,7 +104,7 @@ func (h handler) actorInboxLike(c *gin.Context, user *storage.User, payload stor
}
actor, hasActor := storage.JSONString(payload, "actor")
object, hasObject := storage.JSONDeepString(payload, "object", "object")
object, hasObject := storage.JSONString(payload, "object")
if !hasActor || !hasObject {
h.logger.Warn("unable to process like activity",
@ -593,6 +593,8 @@ func (h handler) actorInboxUndo(c *gin.Context, user *storage.User, payload stor
switch innerType {
case "Follow":
h.actorInboxUndoFollow(c, user, payload)
case "Like":
h.actorInboxUndoLike(c, user, payload)
default:
h.logger.Warn("User received unexpected undo payload type", zap.String("type", innerType), zap.String("user", user.Name))
c.Status(http.StatusOK)

View File

@ -50,6 +50,7 @@ type viewObjectContext struct {
LinkReplyURL string
LinkAnnounce bool
LinkLike bool
LinkDelete bool
AttributedTo string
@ -81,6 +82,7 @@ type viewObjectContext struct {
ViewerAnnounced bool
TotalAnnounces int
TotalReplies int
TotalLikes int
Error string