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