From 38ba4d9c7504ddec3639998394f933b959b752da Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 10 Mar 2024 18:38:42 +0800 Subject: [PATCH] :sparkles: Attachments --- pkg/models/articles.go | 17 +-- pkg/models/attachments.go | 3 +- pkg/models/feed.go | 4 +- pkg/models/moments.go | 17 +-- pkg/models/posts.go | 5 +- pkg/server/articles_api.go | 2 +- pkg/server/comments_api.go | 26 ++--- pkg/server/feed_api.go | 76 ++++++++++-- pkg/server/moments_api.go | 10 +- pkg/services/attachments.go | 17 ++- .../src/components/posts/PostAttachment.vue | 45 ++++++++ pkg/views/src/components/posts/PostItem.vue | 3 + .../src/components/publish/ArticleEditor.vue | 38 +++++- .../src/components/publish/MomentEditor.vue | 12 +- .../src/components/publish/parts/Media.vue | 109 ++++++++++++++++++ .../publish/parts/PlannedPublish.vue | 2 +- pkg/views/src/layouts/master.vue | 2 +- 17 files changed, 328 insertions(+), 60 deletions(-) create mode 100644 pkg/views/src/components/posts/PostAttachment.vue create mode 100644 pkg/views/src/components/publish/parts/Media.vue diff --git a/pkg/models/articles.go b/pkg/models/articles.go index a10605e..1ebabfa 100644 --- a/pkg/models/articles.go +++ b/pkg/models/articles.go @@ -3,14 +3,15 @@ package models type Article struct { PostBase - Title string `json:"title"` - Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"` - Categories []Category `json:"categories" gorm:"many2many:article_categories"` - Reactions []Reaction `json:"reactions"` - Description string `json:"description"` - Content string `json:"content"` - RealmID *uint `json:"realm_id"` - Realm *Realm `json:"realm"` + Title string `json:"title"` + Hashtags []Tag `json:"tags" gorm:"many2many:article_tags"` + Categories []Category `json:"categories" gorm:"many2many:article_categories"` + Reactions []Reaction `json:"reactions"` + Attachments []Attachment `json:"attachments"` + Description string `json:"description"` + Content string `json:"content"` + RealmID *uint `json:"realm_id"` + Realm *Realm `json:"realm"` Comments []Comment `json:"comments" gorm:"foreignKey:ArticleID"` } diff --git a/pkg/models/attachments.go b/pkg/models/attachments.go index 0bbcbd5..300be2b 100644 --- a/pkg/models/attachments.go +++ b/pkg/models/attachments.go @@ -2,8 +2,9 @@ package models import ( "fmt" - "github.com/spf13/viper" "path/filepath" + + "github.com/spf13/viper" ) type AttachmentType = uint8 diff --git a/pkg/models/feed.go b/pkg/models/feed.go index e68b7fe..f1b22db 100644 --- a/pkg/models/feed.go +++ b/pkg/models/feed.go @@ -15,6 +15,8 @@ type Feed struct { AuthorID uint `json:"author_id"` RealmID *uint `json:"realm_id"` - Author Account `json:"author" gorm:"embedded"` + Author Account `json:"author" gorm:"embedded"` + + Attachments []Attachment `json:"attachments" gorm:"-"` ReactionList map[string]int64 `json:"reaction_list" gorm:"-"` } diff --git a/pkg/models/moments.go b/pkg/models/moments.go index 6d8b633..f25b3c0 100644 --- a/pkg/models/moments.go +++ b/pkg/models/moments.go @@ -3,14 +3,15 @@ package models type Moment struct { PostBase - Content string `json:"content"` - Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"` - Categories []Category `json:"categories" gorm:"many2many:moment_categories"` - Reactions []Reaction `json:"reactions"` - RealmID *uint `json:"realm_id"` - RepostID *uint `json:"repost_id"` - Realm *Realm `json:"realm"` - RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"` + Content string `json:"content"` + Hashtags []Tag `json:"tags" gorm:"many2many:moment_tags"` + Categories []Category `json:"categories" gorm:"many2many:moment_categories"` + Reactions []Reaction `json:"reactions"` + Attachments []Attachment `json:"attachments"` + RealmID *uint `json:"realm_id"` + RepostID *uint `json:"repost_id"` + Realm *Realm `json:"realm"` + RepostTo *Moment `json:"repost_to" gorm:"foreignKey:RepostID"` Comments []Comment `json:"comments" gorm:"foreignKey:MomentID"` } diff --git a/pkg/models/posts.go b/pkg/models/posts.go index f40ec89..94d1cef 100644 --- a/pkg/models/posts.go +++ b/pkg/models/posts.go @@ -15,9 +15,8 @@ type PostReactInfo struct { type PostBase struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` - Attachments []Attachment `json:"attachments"` - PublishedAt *time.Time `json:"published_at"` + Alias string `json:"alias" gorm:"uniqueIndex"` + PublishedAt *time.Time `json:"published_at"` AuthorID uint `json:"author_id"` Author Account `json:"author"` diff --git a/pkg/server/articles_api.go b/pkg/server/articles_api.go index 10c7970..6f78d02 100644 --- a/pkg/server/articles_api.go +++ b/pkg/server/articles_api.go @@ -45,12 +45,12 @@ func createArticle(c *fiber.Ctx) error { item := &models.Article{ PostBase: models.PostBase{ Alias: data.Alias, - Attachments: data.Attachments, PublishedAt: data.PublishedAt, AuthorID: user.ID, }, Hashtags: data.Hashtags, Categories: data.Categories, + Attachments: data.Attachments, Title: data.Title, Description: data.Description, Content: data.Content, diff --git a/pkg/server/comments_api.go b/pkg/server/comments_api.go index eaad8f5..baaeef1 100644 --- a/pkg/server/comments_api.go +++ b/pkg/server/comments_api.go @@ -56,13 +56,12 @@ func createComment(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) var data struct { - Alias string `json:"alias" form:"alias"` - Content string `json:"content" form:"content" validate:"required"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` - ReplyTo uint `json:"reply_to" form:"reply_to"` + Alias string `json:"alias" form:"alias"` + Content string `json:"content" form:"content" validate:"required"` + PublishedAt *time.Time `json:"published_at" form:"published_at"` + Hashtags []models.Tag `json:"hashtags" form:"hashtags"` + Categories []models.Category `json:"categories" form:"categories"` + ReplyTo uint `json:"reply_to" form:"reply_to"` } if err := BindAndValidate(c, &data); err != nil { @@ -74,7 +73,6 @@ func createComment(c *fiber.Ctx) error { item := &models.Comment{ PostBase: models.PostBase{ Alias: data.Alias, - Attachments: data.Attachments, PublishedAt: data.PublishedAt, AuthorID: user.ID, }, @@ -133,12 +131,11 @@ func editComment(c *fiber.Ctx) error { id, _ := c.ParamsInt("commentId", 0) var data struct { - Alias string `json:"alias" form:"alias" validate:"required"` - Content string `json:"content" form:"content" validate:"required"` - PublishedAt *time.Time `json:"published_at" form:"published_at"` - Hashtags []models.Tag `json:"hashtags" form:"hashtags"` - Categories []models.Category `json:"categories" form:"categories"` - Attachments []models.Attachment `json:"attachments" form:"attachments"` + Alias string `json:"alias" form:"alias" validate:"required"` + Content string `json:"content" form:"content" validate:"required"` + PublishedAt *time.Time `json:"published_at" form:"published_at"` + Hashtags []models.Tag `json:"hashtags" form:"hashtags"` + Categories []models.Category `json:"categories" form:"categories"` } if err := BindAndValidate(c, &data); err != nil { @@ -160,7 +157,6 @@ func editComment(c *fiber.Ctx) error { item.PublishedAt = data.PublishedAt item.Hashtags = data.Hashtags item.Categories = data.Categories - item.Attachments = data.Attachments if item, err := services.EditPost(item); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) diff --git a/pkg/server/feed_api.go b/pkg/server/feed_api.go index 88c1110..a4e4da0 100644 --- a/pkg/server/feed_api.go +++ b/pkg/server/feed_api.go @@ -47,22 +47,28 @@ func listFeed(c *fiber.Ctx) error { commentTable := viper.GetString("database.prefix") + "comments" reactionTable := viper.GetString("database.prefix") + "reactions" - database.C.Raw(fmt.Sprintf(`SELECT feed.*, author.*, - COALESCE(comment_count, 0) as comment_count, - COALESCE(reaction_count, 0) as reaction_count - FROM (? UNION ALL ?) as feed - INNER JOIN %s as author ON author_id = author.id - LEFT JOIN (SELECT article_id, moment_id, COUNT(*) as comment_count + database.C.Raw( + fmt.Sprintf(`SELECT feed.*, author.*, + COALESCE(comment_count, 0) AS comment_count, + COALESCE(reaction_count, 0) AS reaction_count + FROM (? UNION ALL ?) AS feed + INNER JOIN %s AS author ON author_id = author.id + LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS comment_count FROM %s - GROUP BY article_id, moment_id) as comments + GROUP BY article_id, moment_id) AS comments ON (feed.model_type = 'article' AND feed.id = comments.article_id) OR (feed.model_type = 'moment' AND feed.id = comments.moment_id) - LEFT JOIN (SELECT article_id, moment_id, COUNT(*) as reaction_count + LEFT JOIN (SELECT article_id, moment_id, COUNT(*) AS reaction_count FROM %s - GROUP BY article_id, moment_id) as reactions + GROUP BY article_id, moment_id) AS reactions ON (feed.model_type = 'article' AND feed.id = reactions.article_id) OR (feed.model_type = 'moment' AND feed.id = reactions.moment_id) - WHERE %s ORDER BY feed.created_at desc LIMIT ? OFFSET ?`, userTable, commentTable, reactionTable, whereCondition), + WHERE %s ORDER BY feed.created_at desc LIMIT ? OFFSET ?`, + userTable, + commentTable, + reactionTable, + whereCondition, + ), database.C.Select(queryArticle).Model(&models.Article{}), database.C.Select(queryMoment).Model(&models.Moment{}), take, @@ -122,6 +128,56 @@ func listFeed(c *fiber.Ctx) error { } } + if !c.QueryBool("noAttachment", false) { + revertAttachment := func(dataset string) error { + var attachments []struct { + models.Attachment + + PostID uint `json:"post_id"` + } + + itemMap := lo.SliceToMap(lo.FilterMap(result, func(item *models.Feed, index int) (*models.Feed, bool) { + return item, item.ModelType == dataset + }), func(item *models.Feed) (uint, *models.Feed) { + return item.ID, item + }) + + idx := lo.Map(lo.Filter(result, func(item *models.Feed, index int) bool { + return item.ModelType == dataset + }), func(item *models.Feed, index int) uint { + return item.ID + }) + + if err := database.C. + Model(&models.Attachment{}). + Select(dataset+"_id as post_id, *"). + Where(dataset+"_id IN (?)", idx). + Scan(&attachments).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + list := map[uint][]models.Attachment{} + for _, info := range attachments { + list[info.PostID] = append(list[info.PostID], info.Attachment) + } + + for k, v := range list { + if post, ok := itemMap[k]; ok { + post.Attachments = v + } + } + + return nil + } + + if err := revertAttachment("article"); err != nil { + return err + } + if err := revertAttachment("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 10a5b2c..6fe5839 100644 --- a/pkg/server/moments_api.go +++ b/pkg/server/moments_api.go @@ -44,14 +44,14 @@ func createMoment(c *fiber.Ctx) error { item := &models.Moment{ PostBase: models.PostBase{ Alias: data.Alias, - Attachments: data.Attachments, PublishedAt: data.PublishedAt, AuthorID: user.ID, }, - Hashtags: data.Hashtags, - Categories: data.Categories, - Content: data.Content, - RealmID: data.RealmID, + Hashtags: data.Hashtags, + Categories: data.Categories, + Attachments: data.Attachments, + Content: data.Content, + RealmID: data.RealmID, } var relatedCount int64 diff --git a/pkg/services/attachments.go b/pkg/services/attachments.go index 2b04a46..0384422 100644 --- a/pkg/services/attachments.go +++ b/pkg/services/attachments.go @@ -1,11 +1,13 @@ package services import ( + "mime/multipart" + "net/http" + "strings" + "code.smartsheep.studio/hydrogen/interactive/pkg/database" "code.smartsheep.studio/hydrogen/interactive/pkg/models" "github.com/google/uuid" - "mime/multipart" - "net/http" ) func NewAttachment(user models.Account, header *multipart.FileHeader) (models.Attachment, error) { @@ -33,6 +35,17 @@ func NewAttachment(user models.Account, header *multipart.FileHeader) (models.At } attachment.Mimetype = http.DetectContentType(fileHeader) + switch strings.Split(attachment.Mimetype, "/")[0] { + case "image": + attachment.Type = models.AttachmentPhoto + case "video": + attachment.Type = models.AttachmentVideo + case "audio": + attachment.Type = models.AttachmentAudio + default: + attachment.Type = models.AttachmentOthers + } + // Save into database err = database.C.Save(&attachment).Error diff --git a/pkg/views/src/components/posts/PostAttachment.vue b/pkg/views/src/components/posts/PostAttachment.vue new file mode 100644 index 0000000..59df4ca --- /dev/null +++ b/pkg/views/src/components/posts/PostAttachment.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/pkg/views/src/components/posts/PostItem.vue b/pkg/views/src/components/posts/PostItem.vue index ad9cc58..92cb550 100644 --- a/pkg/views/src/components/posts/PostItem.vue +++ b/pkg/views/src/components/posts/PostItem.vue @@ -18,6 +18,8 @@ + + () diff --git a/pkg/views/src/components/publish/ArticleEditor.vue b/pkg/views/src/components/publish/ArticleEditor.vue index 3e7cac9..95afd66 100644 --- a/pkg/views/src/components/publish/ArticleEditor.vue +++ b/pkg/views/src/components/publish/ArticleEditor.vue @@ -67,6 +67,20 @@ + + + + @@ -74,8 +88,13 @@ + Your article has been published. + + Uploading your media, please stand by... + + Something went wrong... {{ error }} @@ -86,8 +105,9 @@ import { request } from "@/scripts/request" import { useEditor } from "@/stores/editor" import { getAtk } from "@/stores/userinfo" import { reactive, ref } from "vue" -import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" import { useRouter } from "vue-router" +import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" +import Media from "@/components/publish/parts/Media.vue" const editor = useEditor() @@ -101,7 +121,8 @@ const data = reactive({ title: "", content: "", description: "", - publishedAt: null + publishedAt: null, + attachments: [] }) const router = useRouter() @@ -109,10 +130,13 @@ const router = useRouter() const error = ref(null) const success = ref(false) const loading = ref(false) +const uploading = ref(false) async function postArticle(evt: SubmitEvent) { const form = evt.target as HTMLFormElement + if (uploading.value) return + if (!data.content) return if (!data.title || !data.description) return if (!data.publishedAt) data.publishedAt = new Date().toISOString() @@ -120,7 +144,7 @@ async function postArticle(evt: SubmitEvent) { loading.value = true const res = await request("/api/p/articles", { method: "POST", - headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` }, + headers: { "Content-Type": "application/json", Authorization: `Bearer ${getAtk()}` }, body: JSON.stringify(data) }) if (res.status === 200) { @@ -149,4 +173,12 @@ async function postArticle(evt: SubmitEvent) { .article-container { max-width: 720px; } + +.snackbar-progress { + margin-left: -16px; + margin-right: -16px; + margin-bottom: -14px; + margin-top: 12px; + width: calc(100% + 64px); +} diff --git a/pkg/views/src/components/publish/MomentEditor.vue b/pkg/views/src/components/publish/MomentEditor.vue index 4e666aa..4efc09b 100644 --- a/pkg/views/src/components/publish/MomentEditor.vue +++ b/pkg/views/src/components/publish/MomentEditor.vue @@ -54,8 +54,13 @@ + Your post has been published. + + Uploading your media, please stand by... + + Something went wrong... {{ error }} @@ -67,6 +72,7 @@ import { useEditor } from "@/stores/editor" import { getAtk } from "@/stores/userinfo" import { reactive, ref } from "vue" import PlannedPublish from "@/components/publish/parts/PlannedPublish.vue" +import Media from "@/components/publish/parts/Media.vue" const editor = useEditor() @@ -77,12 +83,14 @@ const dialogs = reactive({ }) const extras = reactive({ - publishedAt: null + publishedAt: null, + attachments: [] }) const error = ref(null) const success = ref(false) const loading = ref(false) +const uploading = ref(false) async function postMoment(evt: SubmitEvent) { const form = evt.target as HTMLFormElement @@ -91,6 +99,8 @@ async function postMoment(evt: SubmitEvent) { if (!extras.publishedAt) data.set("published_at", new Date().toISOString()) else data.set("published_at", extras.publishedAt) + extras.attachments.forEach((item) => data.append("attachments[]", item)) + loading.value = true const res = await request("/api/p/moments", { method: "POST", diff --git a/pkg/views/src/components/publish/parts/Media.vue b/pkg/views/src/components/publish/parts/Media.vue new file mode 100644 index 0000000..17e1f2b --- /dev/null +++ b/pkg/views/src/components/publish/parts/Media.vue @@ -0,0 +1,109 @@ + + + diff --git a/pkg/views/src/components/publish/parts/PlannedPublish.vue b/pkg/views/src/components/publish/parts/PlannedPublish.vue index 7190173..bc8f81d 100644 --- a/pkg/views/src/components/publish/parts/PlannedPublish.vue +++ b/pkg/views/src/components/publish/parts/PlannedPublish.vue @@ -12,7 +12,7 @@ class="mt-2" label="Publish date" hint="Your post will hidden for public before this time. Leave blank will publish immediately" - variant="outlined" + variant="solo-filled" type="datetime-local" :model-value="props.value" @update:model-value="(val) => emits('update:value', val)" diff --git a/pkg/views/src/layouts/master.vue b/pkg/views/src/layouts/master.vue index 2cde299..41852af 100644 --- a/pkg/views/src/layouts/master.vue +++ b/pkg/views/src/layouts/master.vue @@ -17,7 +17,7 @@
We just released the brand new design system and user interface! - Contribute our survey + Take a survey