From 56a9c765b94d69a1291e4528ce925d027dddbb4b Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 3 Feb 2024 00:50:23 +0800 Subject: [PATCH] :sparkles: Reacting --- pkg/database/migrator.go | 2 + pkg/models/accounts.go | 18 +++--- pkg/models/posts.go | 24 +++++--- pkg/models/reactions.go | 19 ++++++ pkg/server/posts_api.go | 70 ++++++++++++++++++++- pkg/server/startup.go | 1 + pkg/services/posts.go | 40 ++++++++++++ pkg/view/src/pages/feed.tsx | 120 ++++++++++++++++++++++-------------- 8 files changed, 229 insertions(+), 65 deletions(-) create mode 100644 pkg/models/reactions.go diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index 4a1ac28..3779e9c 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -12,6 +12,8 @@ func RunMigration(source *gorm.DB) error { &models.Category{}, &models.Tag{}, &models.Post{}, + &models.PostLike{}, + &models.PostDislike{}, ); err != nil { return err } diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index 7e511e3..f054e2f 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -6,12 +6,14 @@ package models type Account struct { BaseModel - Name string `json:"name"` - Avatar string `json:"avatar"` - Description string `json:"description"` - EmailAddress string `json:"email_address"` - PowerLevel int `json:"power_level"` - Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` - Realms []Realm `json:"realms"` - ExternalID uint `json:"external_id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Description string `json:"description"` + EmailAddress string `json:"email_address"` + PowerLevel int `json:"power_level"` + Posts []Post `json:"posts" gorm:"foreignKey:AuthorID"` + LikedPosts []PostLike `json:"liked_posts"` + DislikedPosts []PostDislike `json:"disliked_posts"` + Realms []Realm `json:"realms"` + ExternalID uint `json:"external_id"` } diff --git a/pkg/models/posts.go b/pkg/models/posts.go index a04dad9..d2583d4 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -5,13 +5,19 @@ import "time" type Post struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` - Title string `json:"title"` - Content string `json:"content"` - Tags []Tag `gorm:"many2many:post_tags"` - Categories []Category `gorm:"many2many:post_categories"` - PublishedAt time.Time `json:"published_at"` - RealmID *uint `json:"realm_id"` - AuthorID uint `json:"author_id"` - Author Account `json:"author"` + Alias string `json:"alias" gorm:"uniqueIndex"` + Title string `json:"title"` + Content string `json:"content"` + Tags []Tag `gorm:"many2many:post_tags"` + Categories []Category `gorm:"many2many:post_categories"` + LikedAccounts []PostLike `json:"liked_accounts"` + DislikedAccounts []PostDislike `json:"disliked_accounts"` + PublishedAt time.Time `json:"published_at"` + RealmID *uint `json:"realm_id"` + AuthorID uint `json:"author_id"` + Author Account `json:"author"` + + // Dynamic Calculating Values + LikeCount int64 `json:"like_count" gorm:"-"` + DislikeCount int64 `json:"dislike_count" gorm:"-"` } diff --git a/pkg/models/reactions.go b/pkg/models/reactions.go new file mode 100644 index 0000000..bb55844 --- /dev/null +++ b/pkg/models/reactions.go @@ -0,0 +1,19 @@ +package models + +import "time" + +type PostLike struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PostID uint `json:"post_id"` + AccountID uint `json:"account_id"` +} + +type PostDislike struct { + ID uint `json:"id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + PostID uint `json:"post_id"` + AccountID uint `json:"account_id"` +} diff --git a/pkg/server/posts_api.go b/pkg/server/posts_api.go index 0d7fb2a..8c4ce88 100644 --- a/pkg/server/posts_api.go +++ b/pkg/server/posts_api.go @@ -4,7 +4,12 @@ import ( "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" "code.smartsheep.studio/hydrogen/interactive/pkg/services" + "fmt" "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + "github.com/samber/lo" + "github.com/spf13/viper" + "strings" ) func listPost(c *fiber.Ctx) error { @@ -14,12 +19,14 @@ func listPost(c *fiber.Ctx) error { var count int64 var posts []models.Post if err := database.C. + Where(&models.Post{RealmID: nil}). Model(&models.Post{}). Count(&count).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } if err := database.C. + Where(&models.Post{RealmID: nil}). Limit(take). Offset(offset). Order("created_at desc"). @@ -28,6 +35,34 @@ func listPost(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } + postIds := lo.Map(posts, func(item models.Post, _ int) uint { + return item.ID + }) + + var reactInfo []struct { + PostID uint + LikeCount int64 + DislikeCount int64 + } + + prefix := viper.GetString("database.prefix") + database.C.Raw(fmt.Sprintf(`SELECT t.id as post_id, + COALESCE(l.like_count, 0) AS like_count, + COALESCE(d.dislike_count, 0) AS dislike_count +FROM %sposts t + LEFT JOIN (SELECT post_id, COUNT(*) AS like_count + FROM %spost_likes + GROUP BY post_id) l ON t.id = l.post_id + LEFT JOIN (SELECT post_id, COUNT(*) AS dislike_count + FROM %spost_dislikes + GROUP BY post_id) d ON t.id = d.post_id +WHERE t.id IN (?)`, prefix, prefix, prefix), postIds).Scan(&reactInfo) + + for idx, info := range reactInfo { + posts[idx].LikeCount = info.LikeCount + posts[idx].DislikeCount = info.DislikeCount + } + return c.JSON(fiber.Map{ "count": count, "data": posts, @@ -39,8 +74,8 @@ func createPost(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) var data struct { - Alias string `json:"alias" validate:"required"` - Title string `json:"title" validate:"required"` + Alias string `json:"alias"` + Title string `json:"title"` Content string `json:"content" validate:"required"` Tags []models.Tag `json:"tags"` Categories []models.Category `json:"categories"` @@ -48,6 +83,8 @@ func createPost(c *fiber.Ctx) error { if err := BindAndValidate(c, &data); err != nil { return err + } else if len(data.Alias) == 0 { + data.Alias = strings.ReplaceAll(uuid.NewString(), "-", "") } post, err := services.NewPost(user, data.Alias, data.Title, data.Content, data.Categories, data.Tags) @@ -57,3 +94,32 @@ func createPost(c *fiber.Ctx) error { return c.JSON(post) } + +func reactPost(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + id, _ := c.ParamsInt("postId", 0) + + var post models.Post + if err := database.C.Where(&models.Post{ + BaseModel: models.BaseModel{ID: uint(id)}, + }).First(&post).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } + + switch strings.ToLower(c.Params("reactType")) { + case "like": + if positive, err := services.LikePost(user, post); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) + } + case "dislike": + if positive, err := services.DislikePost(user, post); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.SendStatus(lo.Ternary(positive, fiber.StatusCreated, fiber.StatusNoContent)) + } + default: + return fiber.NewError(fiber.StatusBadRequest, "unsupported reaction") + } +} diff --git a/pkg/server/startup.go b/pkg/server/startup.go index a41e4c5..b0666a4 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -64,6 +64,7 @@ func NewServer() { api.Get("/posts", listPost) api.Post("/posts", auth, createPost) + api.Post("/posts/:postId/react/:reactType", auth, reactPost) } A.Use("/", cache.New(cache.Config{ diff --git a/pkg/services/posts.go b/pkg/services/posts.go index 5ad700e..26e93c6 100644 --- a/pkg/services/posts.go +++ b/pkg/services/posts.go @@ -3,6 +3,8 @@ package services import ( "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" + "errors" + "gorm.io/gorm" ) func NewPost( @@ -57,3 +59,41 @@ func NewPostWithRealm( return post, nil } + +func LikePost(user models.Account, post models.Post) (bool, error) { + var like models.PostLike + if err := database.C.Where(&models.PostLike{ + AccountID: user.ID, + PostID: post.ID, + }).First(&like).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return true, err + } + like = models.PostLike{ + AccountID: user.ID, + PostID: post.ID, + } + return true, database.C.Save(&like).Error + } else { + return false, database.C.Delete(&like).Error + } +} + +func DislikePost(user models.Account, post models.Post) (bool, error) { + var dislike models.PostDislike + if err := database.C.Where(&models.PostDislike{ + AccountID: user.ID, + PostID: post.ID, + }).First(&dislike).Error; err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + return true, err + } + dislike = models.PostDislike{ + AccountID: user.ID, + PostID: post.ID, + } + return true, database.C.Save(&dislike).Error + } else { + return false, database.C.Delete(&dislike).Error + } +} diff --git a/pkg/view/src/pages/feed.tsx b/pkg/view/src/pages/feed.tsx index e76f151..029e718 100644 --- a/pkg/view/src/pages/feed.tsx +++ b/pkg/view/src/pages/feed.tsx @@ -1,5 +1,5 @@ import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; -import { createSignal, For, Show } from "solid-js"; +import { createEffect, createSignal, For, Show } from "solid-js"; import styles from "./feed.module.css"; @@ -9,6 +9,7 @@ export default function DashboardPage() { const [error, setError] = createSignal(null); const [loading, setLoading] = createSignal(true); const [submitting, setSubmitting] = createSignal(false); + const [reacting, setReacting] = createSignal(false); const [posts, setPosts] = createSignal([]); const [postCount, setPostCount] = createSignal(0); @@ -32,6 +33,8 @@ export default function DashboardPage() { setLoading(false); } + createEffect(() => readPosts(), [page()]); + async function doPost(evt: SubmitEvent) { evt.preventDefault(); @@ -62,7 +65,20 @@ export default function DashboardPage() { setSubmitting(false); } - readPosts(); + async function reactPost(item: any, type: string) { + setReacting(true); + const res = await fetch(`/api/posts/${item.id}/react/${type}`, { + method: "POST", + headers: { "Authorization": `Bearer ${getAtk()}` } + }); + if (res.status !== 201 && res.status !== 204) { + setError(await res.text()); + } else { + await readPosts(); + setError(null); + } + setReacting(false); + } return (
@@ -118,57 +134,69 @@ export default function DashboardPage() {
- }> - - {item =>
+ + {item =>
-
-
-
- {item.author.name.substring(0, 1)}}> - avatar - -
-
-
-
-

{item.author.name}

-

{item.author.description}

-
+
+
+
+ {item.author.name.substring(0, 1)}}> + avatar +
- -
-

{item.title}

-
{item.content}
-
- -
-
- +
+
+

{item.author.name}

+

{item.author.description}

-
- -
- -
+
+ +
+

{item.title}

+
{item.content}
+
+ +
+ +
+
-
} - +
+ +
+ + + + + +
+ +
} + + + +
+

+

Creating fake news...

+