diff --git a/pkg/models/articles.go b/pkg/models/articles.go index 138cae6..a10605e 100644 --- a/pkg/models/articles.go +++ b/pkg/models/articles.go @@ -15,26 +15,26 @@ type Article struct { Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"` } -func (p Article) GetReplyTo() PostInterface { +func (p *Article) GetReplyTo() PostInterface { return nil } -func (p Article) GetRepostTo() PostInterface { +func (p *Article) GetRepostTo() PostInterface { return nil } -func (p Article) GetHashtags() []Tag { +func (p *Article) GetHashtags() []Tag { return p.Hashtags } -func (p Article) GetCategories() []Category { +func (p *Article) GetCategories() []Category { return p.Categories } -func (p Article) SetHashtags(tags []Tag) { +func (p *Article) SetHashtags(tags []Tag) { p.Hashtags = tags } -func (p Article) SetCategories(categories []Category) { +func (p *Article) SetCategories(categories []Category) { p.Categories = categories } diff --git a/pkg/models/comments.go b/pkg/models/comments.go index 6d8387e..cf1d37b 100644 --- a/pkg/models/comments.go +++ b/pkg/models/comments.go @@ -16,22 +16,22 @@ type Comment struct { Moment *Moment `json:"moment"` } -func (p Comment) GetReplyTo() PostInterface { +func (p *Comment) GetReplyTo() PostInterface { return p.ReplyTo } -func (p Comment) GetHashtags() []Tag { +func (p *Comment) GetHashtags() []Tag { return p.Hashtags } -func (p Comment) GetCategories() []Category { +func (p *Comment) GetCategories() []Category { return p.Categories } -func (p Comment) SetHashtags(tags []Tag) { +func (p *Comment) SetHashtags(tags []Tag) { p.Hashtags = tags } -func (p Comment) SetCategories(categories []Category) { +func (p *Comment) SetCategories(categories []Category) { p.Categories = categories } diff --git a/pkg/models/moments.go b/pkg/models/moments.go index c6affb3..6d8b633 100644 --- a/pkg/models/moments.go +++ b/pkg/models/moments.go @@ -15,26 +15,26 @@ type Moment struct { Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"` } -func (p Moment) GetRepostTo() PostInterface { +func (p *Moment) GetRepostTo() PostInterface { return p.RepostTo } -func (p Moment) GetRealm() *Realm { +func (p *Moment) GetRealm() *Realm { return p.Realm } -func (p Moment) GetHashtags() []Tag { +func (p *Moment) GetHashtags() []Tag { return p.Hashtags } -func (p Moment) GetCategories() []Category { +func (p *Moment) GetCategories() []Category { return p.Categories } -func (p Moment) SetHashtags(tags []Tag) { +func (p *Moment) SetHashtags(tags []Tag) { p.Hashtags = tags } -func (p Moment) SetCategories(categories []Category) { +func (p *Moment) SetCategories(categories []Category) { p.Categories = categories } diff --git a/pkg/models/posts.go b/pkg/models/posts.go index b326e31..f40ec89 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -22,29 +22,34 @@ type PostBase struct { AuthorID uint `json:"author_id"` Author Account `json:"author"` - // TODO Give the reactions & replies & reposts info back + // Dynamic Calculated Values + ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` } -func (p PostBase) GetID() uint { +func (p *PostBase) GetID() uint { return p.ID } -func (p PostBase) GetReplyTo() PostInterface { +func (p *PostBase) GetReplyTo() PostInterface { return nil } -func (p PostBase) GetRepostTo() PostInterface { +func (p *PostBase) GetRepostTo() PostInterface { return nil } -func (p PostBase) GetAuthor() Account { +func (p *PostBase) GetAuthor() Account { return p.Author } -func (p PostBase) GetRealm() *Realm { +func (p *PostBase) GetRealm() *Realm { return nil } +func (p *PostBase) SetReactionList(list map[string]int64) { + p.ReactionList = list +} + type PostInterface interface { GetID() uint GetHashtags() []Tag @@ -56,4 +61,5 @@ type PostInterface interface { SetHashtags([]Tag) SetCategories([]Category) + SetReactionList(map[string]int64) } diff --git a/pkg/server/articles_api.go b/pkg/server/articles_api.go index 9132fcc..8e44a89 100644 --- a/pkg/server/articles_api.go +++ b/pkg/server/articles_api.go @@ -12,8 +12,8 @@ import ( "github.com/samber/lo" ) -func contextArticle() *services.PostTypeContext[models.Article] { - return &services.PostTypeContext[models.Article]{ +func contextArticle() *services.PostTypeContext[*models.Article] { + return &services.PostTypeContext[*models.Article]{ Tx: database.C, TypeName: "Article", CanReply: false, @@ -31,6 +31,11 @@ func getArticle(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, err.Error()) } + item.ReactionList, err = mx.CountReactions(item.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return c.JSON(item) } @@ -102,7 +107,7 @@ func createArticle(c *fiber.Ctx) error { mx := contextArticle() - item := models.Article{ + item := &models.Article{ PostBase: models.PostBase{ Alias: data.Alias, Attachments: data.Attachments, @@ -192,7 +197,7 @@ func reactArticle(c *fiber.Ctx) error { mx := contextArticle() - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -218,7 +223,7 @@ func deleteArticle(c *fiber.Ctx) error { mx := contextArticle().FilterAuthor(user.ID) - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } diff --git a/pkg/server/comments_api.go b/pkg/server/comments_api.go index 1e33e69..d76dfef 100644 --- a/pkg/server/comments_api.go +++ b/pkg/server/comments_api.go @@ -13,8 +13,8 @@ import ( "github.com/samber/lo" ) -func contextComment() *services.PostTypeContext[models.Comment] { - return &services.PostTypeContext[models.Comment]{ +func contextComment() *services.PostTypeContext[*models.Comment] { + return &services.PostTypeContext[*models.Comment]{ Tx: database.C, TypeName: "Comment", CanReply: false, @@ -32,6 +32,11 @@ func getComment(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, err.Error()) } + item.ReactionList, err = mx.CountReactions(item.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return c.JSON(item) } @@ -103,7 +108,7 @@ func createComment(c *fiber.Ctx) error { mx := contextComment() - item := models.Comment{ + item := &models.Comment{ PostBase: models.PostBase{ Alias: data.Alias, Attachments: data.Attachments, @@ -199,7 +204,7 @@ func reactComment(c *fiber.Ctx) error { mx := contextComment() - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -225,7 +230,7 @@ func deleteComment(c *fiber.Ctx) error { mx := contextComment().FilterAuthor(user.ID) - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } diff --git a/pkg/server/feed_api.go b/pkg/server/feed_api.go index 0f93982..db5307c 100644 --- a/pkg/server/feed_api.go +++ b/pkg/server/feed_api.go @@ -6,6 +6,7 @@ import ( "code.smartsheep.studio/hydrogen/interactive/pkg/models" "fmt" "github.com/gofiber/fiber/v2" + "github.com/samber/lo" "github.com/spf13/viper" ) @@ -22,7 +23,8 @@ type FeedItem struct { AuthorID uint `json:"author_id"` RealmID *uint `json:"realm_id"` - Author models.Account `json:"author" gorm:"embedded"` + Author models.Account `json:"author" gorm:"embedded"` + ReactionList map[string]int64 `json:"reaction_list"` } const ( @@ -56,7 +58,7 @@ func listFeed(c *fiber.Ctx) error { } } - var result []FeedItem + var result []*FeedItem userTable := viper.GetString("database.prefix") + "accounts" commentTable := viper.GetString("database.prefix") + "comments" @@ -84,6 +86,59 @@ func listFeed(c *fiber.Ctx) error { offset, ).Scan(&result) + if !c.QueryBool("noReact", false) { + var reactions []struct { + PostID uint + Symbol string + Count int64 + } + + revertReaction := func(dataset string) error { + itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *FeedItem, index int) (*FeedItem, bool) { + return item, item.ModelType == dataset + }), func(item *FeedItem) (uint, *FeedItem) { + return item.ID, item + }) + + idx := lo.Map(lo.Filter(result, func(item *FeedItem, index int) bool { + return item.ModelType == dataset + }), func(item *FeedItem, index int) uint { + return item.ID + }) + + if err := database.C.Model(&models.Reaction{}). + Select(dataset+"_id as post_id, symbol, COUNT(id) as count"). + Where(dataset+"_id IN (?)", idx). + Group("post_id, symbol"). + Scan(&reactions).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + list := map[uint]map[string]int64{} + for _, info := range reactions { + if _, ok := list[info.PostID]; !ok { + list[info.PostID] = make(map[string]int64) + } + list[info.PostID][info.Symbol] = info.Count + } + + for k, v := range list { + if post, ok := itemMap[k]; ok { + post.ReactionList = v + } + } + + return nil + } + + if err := revertReaction("article"); err != nil { + return err + } + if err := revertReaction("moment"); err != nil { + return err + } + } + var count int64 database.C.Raw(`SELECT COUNT(*) FROM (? UNION ALL ?) as feed`, database.C.Select(queryArticle).Model(&models.Article{}), diff --git a/pkg/server/moments_api.go b/pkg/server/moments_api.go index 38d8d37..c3ec2b0 100644 --- a/pkg/server/moments_api.go +++ b/pkg/server/moments_api.go @@ -12,8 +12,8 @@ import ( "github.com/samber/lo" ) -func contextMoment() *services.PostTypeContext[models.Moment] { - return &services.PostTypeContext[models.Moment]{ +func contextMoment() *services.PostTypeContext[*models.Moment] { + return &services.PostTypeContext[*models.Moment]{ Tx: database.C, TypeName: "Moment", CanReply: false, @@ -31,6 +31,11 @@ func getMoment(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, err.Error()) } + item.ReactionList, err = mx.CountReactions(item.ID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + return c.JSON(item) } @@ -101,7 +106,7 @@ func createMoment(c *fiber.Ctx) error { mx := contextMoment() - item := models.Moment{ + item := &models.Moment{ PostBase: models.PostBase{ Alias: data.Alias, Attachments: data.Attachments, @@ -197,7 +202,7 @@ func reactMoment(c *fiber.Ctx) error { mx := contextMoment() - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } @@ -223,7 +228,7 @@ func deleteMoment(c *fiber.Ctx) error { mx := contextMoment().FilterAuthor(user.ID) - item, err := mx.Get(uint(id)) + item, err := mx.Get(uint(id), true) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) } diff --git a/pkg/services/posts.go b/pkg/services/posts.go index 63ff1c2..78480cd 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -1,5 +1,6 @@ package services +import "C" import ( "code.smartsheep.studio/hydrogen/identity/pkg/grpc/proto" "code.smartsheep.studio/hydrogen/interactive/pkg/database" @@ -33,12 +34,16 @@ func (v *PostTypeContext[T]) GetTableName(plural ...bool) string { } } -func (v *PostTypeContext[T]) Preload() *PostTypeContext[T] { +func (v *PostTypeContext[T]) Preload(noComments ...bool) *PostTypeContext[T] { v.Tx.Preload("Author"). Preload("Attachments"). Preload("Categories"). Preload("Hashtags") + if len(noComments) <= 0 || !noComments[0] { + v.Tx = v.Tx.Preload("Comments") + } + if v.CanReply { v.Tx.Preload("ReplyTo") } @@ -98,18 +103,18 @@ func (v *PostTypeContext[T]) SortCreatedAt(order string) *PostTypeContext[T] { return v } -func (v *PostTypeContext[T]) GetViaAlias(alias string) (T, error) { +func (v *PostTypeContext[T]) GetViaAlias(alias string, noComments ...bool) (T, error) { var item T - if err := v.Preload().Tx.Where("alias = ?", alias).First(&item).Error; err != nil { + if err := v.Preload(noComments...).Tx.Where("alias = ?", alias).First(&item).Error; err != nil { return item, err } return item, nil } -func (v *PostTypeContext[T]) Get(id uint) (T, error) { +func (v *PostTypeContext[T]) Get(id uint, noComments ...bool) (T, error) { var item T - if err := v.Preload().Tx.Where("id = ?", id).First(&item).Error; err != nil { + if err := v.Preload(noComments...).Tx.Where("id = ?", id).First(&item).Error; err != nil { return item, err } @@ -126,6 +131,28 @@ func (v *PostTypeContext[T]) Count() (int64, error) { return count, nil } +func (v *PostTypeContext[T]) CountReactions(id uint) (map[string]int64, error) { + var reactions []struct { + Symbol string + Count int64 + } + + if err := database.C.Model(&models.Reaction{}). + Select("symbol, COUNT(id) as count"). + Where(strings.ToLower(v.TypeName)+"_id = ?", id). + Group("symbol"). + Scan(&reactions).Error; err != nil { + return map[string]int64{}, err + } + + return lo.SliceToMap(reactions, func(item struct { + Symbol string + Count int64 + }) (string, int64) { + return item.Symbol, item.Count + }), nil +} + func (v *PostTypeContext[T]) List(take int, offset int, noReact ...bool) ([]T, error) { if take > 20 { take = 20 @@ -136,6 +163,44 @@ func (v *PostTypeContext[T]) List(take int, offset int, noReact ...bool) ([]T, e return items, err } + idx := lo.Map(items, func(item T, index int) uint { + return item.GetID() + }) + + if len(noReact) <= 0 || !noReact[0] { + var reactions []struct { + PostID uint + Symbol string + Count int64 + } + + if err := database.C.Model(&models.Reaction{}). + Select(strings.ToLower(v.TypeName)+"_id as post_id, symbol, COUNT(id) as count"). + Where(strings.ToLower(v.TypeName)+"_id IN (?)", idx). + Group("post_id, symbol"). + Scan(&reactions).Error; err != nil { + return items, err + } + + itemMap := lo.SliceToMap(items, func(item T) (uint, T) { + return item.GetID(), item + }) + + list := map[uint]map[string]int64{} + for _, info := range reactions { + if _, ok := list[info.PostID]; !ok { + list[info.PostID] = make(map[string]int64) + } + list[info.PostID][info.Symbol] = info.Count + } + + for k, v := range list { + if post, ok := itemMap[k]; ok { + post.SetReactionList(v) + } + } + } + return items, nil } diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index 94cbdbc..66216c5 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -30,7 +30,7 @@ open-on-hover open-on-click :open-delay="0" - :close-delay="1850" + :close-delay="0" location="top" transition="scroll-y-reverse-transition" >