diff --git a/layouts/nav/vertical-nav.tsx b/layouts/nav/vertical-nav.tsx index 75f7ad9c..3c77fe3d 100644 --- a/layouts/nav/vertical-nav.tsx +++ b/layouts/nav/vertical-nav.tsx @@ -15,7 +15,7 @@ import { import { useViewportScroll } from "framer-motion" import NextLink from "next/link" import React, { useEffect, useState } from "react" - import { FaGithub, FaSearch } from "react-icons/fa" + import { FaBell, FaGithub, FaSearch } from "react-icons/fa" import Logo, { LogoIcon } from "src/components/logo" import { MobileNavButton, MobileNavContent } from "./mobile-nav" import AlgoliaSearch from "src/components/search/algolia-search" @@ -27,6 +27,7 @@ import { import { getSvgIcon } from "components/svg-icon" import { navLinks } from "src/data/links" import { requestApi } from "utils/axios/request" +import Notification from "components/notification" @@ -102,6 +103,7 @@ import { requestApi } from "utils/axios/request" icon={} /> + {/* { + const [filter, setFilter]= useState(filters[0]) + const [notifications,setNotifications]: [Notification[],any] = useState([]) + const stackBorderColor = useColorModeValue(userCustomTheme.borderColor.light, userCustomTheme.borderColor.dark) + + useEffect(() => { + initData() + },[]) + + const initData = async () => { + await getNotifications() + await requestApi.post(`/notifications/unread`) + } + + const getNotifications = async (f?) => { + const res = await requestApi.get(`/notifications/list/${f ? f.type : filter.type}`) + setNotifications(res.data) + } + + const onFilterChange = (f) => { + setFilter(f) + getNotifications(f) + } + + return ( + <> + + + + + + + Notifications + {getSvgIcon("bell")} + + + + + { + { + filters.map(t => + onFilterChange(t)}> + {t.label} + {getSvgIcon(t.icon,'1rem')} + ) + } + } + + {notifications.length !== 0 + ? + }> + {notifications.map((p,i) => + + + + + {getUserName(p.user)} + {p.title} + + {p.subTitle && {p.subTitle}} + {moment(p.created).fromNow()} {!p.read&& unread} + + )} + + : + + } + + + + + + + ) + } + + export default NotificationPage + + \ No newline at end of file diff --git a/server/internal/api/notification.go b/server/internal/api/notification.go new file mode 100644 index 00000000..ca1c2a71 --- /dev/null +++ b/server/internal/api/notification.go @@ -0,0 +1,41 @@ +package api + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/notification" + "github.com/imdotdev/im.dev/server/internal/user" + "github.com/imdotdev/im.dev/server/pkg/common" +) + +func GetNotifications(c *gin.Context) { + tp, _ := strconv.Atoi(c.Param("type")) + u := user.CurrentUser(c) + + nos, err := notification.Query(u, tp) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(nos)) +} + +func GetUnread(c *gin.Context) { + u := user.CurrentUser(c) + count := notification.QueryUnRead(u.ID) + + c.JSON(http.StatusOK, common.RespSuccess(count)) +} + +func ResetUnread(c *gin.Context) { + u := user.CurrentUser(c) + err := notification.ResetUnRead(u.ID) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + c.JSON(http.StatusOK, common.RespSuccess(nil)) +} diff --git a/server/internal/interaction/follow.go b/server/internal/interaction/follow.go index 98357673..feecb1f0 100644 --- a/server/internal/interaction/follow.go +++ b/server/internal/interaction/follow.go @@ -66,9 +66,9 @@ func Follow(targetID string, userId string) *e.Error { if !followed { if models.GetIDType(targetID) == models.IDTypeUser { - notification.Send(targetID, "", models.NotificationLike, targetID, userId) + notification.Send(targetID, "", models.NotificationFollow, targetID, userId) } else { - notification.Send("", targetID, models.NotificationLike, targetID, userId) + notification.Send("", targetID, models.NotificationFollow, targetID, userId) } } diff --git a/server/internal/notification/notification.go b/server/internal/notification/notification.go index 1225b609..2c4ae297 100644 --- a/server/internal/notification/notification.go +++ b/server/internal/notification/notification.go @@ -1,11 +1,14 @@ package notification import ( + "database/sql" + "net/http" "time" "github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/log" + "github.com/imdotdev/im.dev/server/pkg/models" ) var logger = log.RootLogger.New("logger", "notification") @@ -27,3 +30,87 @@ func Send(userID, orgID string, noType int, noID string, operatorID string) { } } } + +func Query(user *models.User, tp int) ([]*models.Notification, *e.Error) { + var rows *sql.Rows + var err error + if tp == 0 { + rows, err = db.Conn.Query("SELECT operator_id,notifiable_type,notifiable_id,read,created FROM user_notification WHERE user_id=? ORDER BY created DESC", user.ID) + } else if tp == models.NotificationComment { + rows, err = db.Conn.Query("SELECT operator_id,notifiable_type,notifiable_id,read,created FROM user_notification WHERE user_id=? and notifiable_type in ('1','6') ORDER BY created DESC", user.ID) + } else { + rows, err = db.Conn.Query("SELECT operator_id,notifiable_type,notifiable_id,read,created FROM user_notification WHERE user_id=? and notifiable_type=? ORDER BY created DESC", user.ID, tp) + } + + if err != nil { + logger.Warn("query notification", "error", err) + return nil, e.New(http.StatusInternalServerError, e.Internal) + } + + nos := make([]*models.Notification, 0) + for rows.Next() { + var operatorID string + var noType int + var noID string + var read bool + var created time.Time + err := rows.Scan(&operatorID, &noType, &noID, &read, &created) + if err != nil { + logger.Warn("scan notification", "error", err) + continue + } + + operator := &models.UserSimple{ID: operatorID} + err = operator.Query() + + no := &models.Notification{Created: created, Type: noType, User: operator, Read: read} + + switch no.Type { + case models.NotificationComment: + no.Title = " commented on your story" + no.SubTitle = models.GetStoryTitle(noID) + no.StoryID = noID + case models.NotificationReply: + no.Title = " replied to your comment" + no.SubTitle = models.GetStoryTitle(noID) + no.StoryID = noID + case models.NotificationLike: + if models.GetIDType(noID) == models.IDTypeComment { + no.Title = " liked your comment" + id := models.GetCommentStoryID(noID) + if id != "" { + no.SubTitle = models.GetStoryTitle(id) + no.StoryID = id + } + } else { + no.Title = " liked your story" + no.SubTitle = models.GetStoryTitle(noID) + no.StoryID = noID + } + case models.NotificationFollow: + no.Title = " started following you" + } + nos = append(nos, no) + } + + return nos, nil +} + +func QueryUnRead(userID string) int { + var count int + err := db.Conn.QueryRow("SELECT count(1) FROM user_notification WHERE user_id=? and read=?", userID, false).Scan(&count) + if err != nil { + logger.Warn("query unread error", "error", err) + } + return count +} + +func ResetUnRead(userID string) *e.Error { + _, err := db.Conn.Exec("UPDATE user_notification SET read=? WHERE user_id=? and read=?", true, userID, false) + if err != nil { + logger.Warn("query notification", "error", err) + return e.New(http.StatusInternalServerError, e.Internal) + } + + return nil +} diff --git a/server/internal/server.go b/server/internal/server.go index cfd7ac26..3de273cb 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -136,6 +136,10 @@ func (s *Server) Start() error { r.POST("/admin/user", IsLogin(), api.AdminSubmitUser) r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers) + // notification apis + r.GET("/notifications/list/:type", IsLogin(), api.GetNotifications) + r.GET("/notifications/unread", IsLogin(), api.GetUnread) + r.POST("/notifications/unread", IsLogin(), api.ResetUnread) // other apis r.GET("/config", GetConfig) r.GET("/navbars", GetNavbars) @@ -144,6 +148,7 @@ func (s *Server) Start() error { r.GET("/sidebars", GetSidebars) r.POST("/sidebar", IsLogin(), SubmitSidebar) + err := router.Run(config.Data.Server.Addr) if err != nil { logger.Crit("start backend server error", "error", err) diff --git a/server/internal/story/comment.go b/server/internal/story/comment.go index d5828b73..74c74032 100644 --- a/server/internal/story/comment.go +++ b/server/internal/story/comment.go @@ -32,28 +32,29 @@ func AddComment(c *models.Comment) *e.Error { err = db.Conn.QueryRow("select story_id from comments where id=?", c.TargetID).Scan(&storyID) if err != nil && err != sql.ErrNoRows { logger.Warn("select comment error", "error", err) - } else { - if storyID == "" { - storyID = c.TargetID - } + return e.New(http.StatusInternalServerError, e.Internal) + } - var nid string - err := db.Conn.QueryRow("SELECT story_id FROM comments_count WHERE story_id=?", storyID).Scan(&nid) - if err != nil && err != sql.ErrNoRows { - logger.Warn("select from comments_count error", "error", err) - return nil - } + if storyID == "" { + storyID = c.TargetID + } - if err == sql.ErrNoRows { - _, err := db.Conn.Exec("INSERT INTO comments_count (story_id,count) VALUES(?,?)", storyID, 1) - if err != nil { - logger.Warn("insert into comments_count error", "error", err) - } - } else { - _, err := db.Conn.Exec("UPDATE comments_count SET count=count+1 WHERE story_id=?", storyID) - if err != nil { - logger.Warn("update comments_count error", "error", err) - } + var nid string + err = db.Conn.QueryRow("SELECT story_id FROM comments_count WHERE story_id=?", storyID).Scan(&nid) + if err != nil && err != sql.ErrNoRows { + logger.Warn("select from comments_count error", "error", err) + return nil + } + + if err == sql.ErrNoRows { + _, err := db.Conn.Exec("INSERT INTO comments_count (story_id,count) VALUES(?,?)", storyID, 1) + if err != nil { + logger.Warn("insert into comments_count error", "error", err) + } + } else { + _, err := db.Conn.Exec("UPDATE comments_count SET count=count+1 WHERE story_id=?", storyID) + if err != nil { + logger.Warn("update comments_count error", "error", err) } } @@ -62,10 +63,10 @@ func AddComment(c *models.Comment) *e.Error { if creator != "" && creator != c.CreatorID { if models.GetIDType(c.TargetID) == models.IDTypeComment { // reply - notification.Send(creator, owner, models.NotificationReply, c.TargetID, c.CreatorID) + notification.Send(creator, owner, models.NotificationReply, storyID, c.CreatorID) } else { // comment - notification.Send(creator, owner, models.NotificationComment, c.TargetID, c.CreatorID) + notification.Send(creator, owner, models.NotificationComment, storyID, c.CreatorID) } } diff --git a/server/internal/story/post.go b/server/internal/story/post.go index 89aa8f37..564831cf 100644 --- a/server/internal/story/post.go +++ b/server/internal/story/post.go @@ -2,7 +2,6 @@ package story import ( "database/sql" - "fmt" "net/http" "strings" "time" @@ -154,7 +153,6 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) { if post.OwnerID != "" { followers, err1 = interaction.GetFollowerIDs(post.OwnerID) if err1 == nil { - fmt.Println(followers) for _, f := range followers { notification.Send("", f, models.NotificationFollow, post.ID, post.CreatorID) } diff --git a/server/pkg/models/comment.go b/server/pkg/models/comment.go index 1ae0d9d2..76d5882c 100644 --- a/server/pkg/models/comment.go +++ b/server/pkg/models/comment.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "time" + + "github.com/imdotdev/im.dev/server/pkg/db" +) type Comment struct { ID string `json:"id"` @@ -30,3 +34,16 @@ func (ar FavorComments) Swap(i, j int) { ar[i], ar[j] = ar[j], ar[i] } func (ar FavorComments) Less(i, j int) bool { return ar[i].Likes > ar[j].Likes } + +func GetCommentStoryID(id string) string { + var t string + db.Conn.QueryRow("SELECT story_id FROM comments WHERE id=?", id).Scan(&t) + + if GetIDType(t) != IDTypeComment { + return t + } + + var t1 string + db.Conn.QueryRow("SELECT story_id FROM comments WHERE id=?", t).Scan(&t1) + return t1 +} diff --git a/server/pkg/models/notification.go b/server/pkg/models/notification.go index 61bf257d..4ea6f154 100644 --- a/server/pkg/models/notification.go +++ b/server/pkg/models/notification.go @@ -1,5 +1,7 @@ package models +import "time" + const ( NotificationComment = 1 NotificationLike = 2 @@ -8,3 +10,13 @@ const ( NotificationFollow = 5 NotificationReply = 6 ) + +type Notification struct { + Type int `json:"type"` + Title string `json:"title"` + SubTitle string `json:"subTitle"` + User *UserSimple `json:"user"` + Read bool `json:"read"` + StoryID string `json:"storyID"` + Created time.Time `json:"created"` +} diff --git a/server/pkg/models/story.go b/server/pkg/models/story.go index deb383e0..e931ffb9 100644 --- a/server/pkg/models/story.go +++ b/server/pkg/models/story.go @@ -108,3 +108,9 @@ func GetStoryCreatorAndOrg(storyID string) (string, string) { return creator, owner } + +func GetStoryTitle(storyID string) string { + var t string + db.Conn.QueryRow("SELECT title FROM story WHERE id=?", storyID).Scan(&t) + return t +} diff --git a/src/components/notification.tsx b/src/components/notification.tsx new file mode 100644 index 00000000..4bddedaa --- /dev/null +++ b/src/components/notification.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from "react" +import { Box, BoxProps, chakra, HStack, IconButton, Text } from "@chakra-ui/react" +import Link from "next/link" +import { ReserveUrls } from "src/data/reserve-urls" +import { FaBell } from "react-icons/fa" +import { requestApi } from "utils/axios/request" + +export const Notification = (props: BoxProps) => { + const [unread, setUnread] = useState(0) + useEffect(() => { + queryUnread() + }, []) + + const queryUnread = async () => { + const res = await requestApi.get("/notifications/unread") + setUnread(res.data) + // await requestApi.post("/notifications/unread") + } + + return ( + <> + + + } + /> + {unread !== 0 && {unread}} + + + + ) + +} +export default Notification diff --git a/src/components/svg-icon.tsx b/src/components/svg-icon.tsx index b20e01b3..73713c83 100644 --- a/src/components/svg-icon.tsx +++ b/src/components/svg-icon.tsx @@ -58,6 +58,15 @@ export function getSvgIcon(name, height = "1.4rem") { case "close": svg = break + case "bell": + svg = + break + case "at": + svg = + break + case "follow": + svg = + break default: break; } diff --git a/src/types/notification.ts b/src/types/notification.ts new file mode 100644 index 00000000..55e5987b --- /dev/null +++ b/src/types/notification.ts @@ -0,0 +1,20 @@ +import { UserSimple } from "./user"; + +export interface Notification { + type: number + title: string + subTitle: string + user: UserSimple + read: boolean + storyID: string + created: string +} + +export enum NotificationType { + Comment = 1, + Like = 2, + Mention = 3, + Publish = 4, + Follow = 5, + Reply = 6 +} \ No newline at end of file