pull/52/head
sunface 4 years ago
parent 0d3ef5505f
commit 807679a4e2

@ -15,7 +15,7 @@ import {
import { useViewportScroll } from "framer-motion" import { useViewportScroll } from "framer-motion"
import NextLink from "next/link" import NextLink from "next/link"
import React, { useEffect, useState } from "react" 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 Logo, { LogoIcon } from "src/components/logo"
import { MobileNavButton, MobileNavContent } from "./mobile-nav" import { MobileNavButton, MobileNavContent } from "./mobile-nav"
import AlgoliaSearch from "src/components/search/algolia-search" import AlgoliaSearch from "src/components/search/algolia-search"
@ -27,6 +27,7 @@ import {
import { getSvgIcon } from "components/svg-icon" import { getSvgIcon } from "components/svg-icon"
import { navLinks } from "src/data/links" import { navLinks } from "src/data/links"
import { requestApi } from "utils/axios/request" import { requestApi } from "utils/axios/request"
import Notification from "components/notification"
@ -102,6 +103,7 @@ import { requestApi } from "utils/axios/request"
icon={<FaGithub />} icon={<FaGithub />}
/> />
</Link> </Link>
<Notification />
<DarkMode fontSize="1.4rem"/> <DarkMode fontSize="1.4rem"/>
<AccountMenu /> <AccountMenu />
{/* <MobileNavButton {/* <MobileNavButton

@ -0,0 +1,121 @@
import {
Heading, HStack, Text, VStack,
Divider,
Wrap,
Image,
useColorModeValue,
Box,
StackDivider,
Tag
} from "@chakra-ui/react"
import SEO from "components/seo"
import siteConfig from "configs/site-config"
import PageContainer1 from "layouts/page-container1"
import React, { useEffect, useState } from "react"
import { IndexSidebar } from 'pages/index'
import Card from "components/card"
import { config } from "configs/config"
import { requestApi } from "utils/axios/request"
import { Story } from "src/types/story"
import { find } from "lodash"
import Empty from "components/empty"
import StoryCard from "components/story/story-card"
import { FaBell } from "react-icons/fa"
import { getSvgIcon } from "components/svg-icon"
import { Notification } from "src/types/notification"
import { getUserName } from "utils/user"
import moment from 'moment'
import userCustomTheme from "theme/user-custom"
import Link from "next/link"
const filters = [
{icon: 'bell',label:'All',type: 0},
{icon: 'comments',label:'Comments',type: 1},
{icon: 'favorites',label:'Likes', type: 2},
{icon: 'follow',label:'Follows', type: 5},
{icon: 'at',label: 'Mentions', type: 3},
{icon: 'post',label: 'Stories', type: 4},
]
const NotificationPage = () => {
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 (
<>
<SEO
title={siteConfig.seo.title}
description={siteConfig.seo.description}
/>
<PageContainer1>
<HStack alignItems="top" p="4" spacing="3">
<VStack alignItems="left" width={["100%", "100%", "70%", "70%"]} spacing="3">
<Card>
<HStack spacing="3">
<Heading size="md">Notifications</Heading>
{getSvgIcon("bell")}
</HStack>
</Card>
<Card p="0">
{<Wrap pt="4" pb="1" pl="4" alignItems="center">
{
filters.map(t =>
<HStack px="2" py="1" spacing="1" mr="2" cursor="pointer" key={t.label} className={t.label===filter?.label ?"tag-bg": null} onClick={() => onFilterChange(t)}>
<Text fontSize=".9rem">{t.label}</Text>
{getSvgIcon(t.icon,'1rem')}
</HStack>)
}
</Wrap>}
<Divider mt="3" mb="5" />
{notifications.length !== 0
?
<VStack alignItems="left" px="4" spacing="5" pb="4" divider={<StackDivider borderColor={stackBorderColor} />}>
{notifications.map((p,i) =>
<HStack key={i} alignItems="top" spacing="4">
<Image src={p.user.avatar} height="45px"/>
<VStack alignItems="left" >
<HStack>
<Link href={`/${p.user.username}`}><Heading fontSize="1rem" cursor="pointer">{getUserName(p.user)}</Heading></Link>
<Text >{p.title}</Text>
</HStack>
{p.subTitle && <Link href={`/${p.user.username}/${p.storyID}`}><Heading size="sm" color="teal" cursor="pointer">{p.subTitle}</Heading></Link>}
<Text fontSize=".8rem" layerStyle="textSecondary">{moment(p.created).fromNow()} {!p.read&& <Tag size="sm" colorScheme="orange">unread</Tag>}</Text>
</VStack>
</HStack>)}
</VStack>
:
<Empty />
}
</Card>
</VStack>
<IndexSidebar />
</HStack>
</PageContainer1>
</>
)
}
export default NotificationPage

@ -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))
}

@ -66,9 +66,9 @@ func Follow(targetID string, userId string) *e.Error {
if !followed { if !followed {
if models.GetIDType(targetID) == models.IDTypeUser { if models.GetIDType(targetID) == models.IDTypeUser {
notification.Send(targetID, "", models.NotificationLike, targetID, userId) notification.Send(targetID, "", models.NotificationFollow, targetID, userId)
} else { } else {
notification.Send("", targetID, models.NotificationLike, targetID, userId) notification.Send("", targetID, models.NotificationFollow, targetID, userId)
} }
} }

@ -1,11 +1,14 @@
package notification package notification
import ( import (
"database/sql"
"net/http"
"time" "time"
"github.com/imdotdev/im.dev/server/pkg/db" "github.com/imdotdev/im.dev/server/pkg/db"
"github.com/imdotdev/im.dev/server/pkg/e" "github.com/imdotdev/im.dev/server/pkg/e"
"github.com/imdotdev/im.dev/server/pkg/log" "github.com/imdotdev/im.dev/server/pkg/log"
"github.com/imdotdev/im.dev/server/pkg/models"
) )
var logger = log.RootLogger.New("logger", "notification") 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
}

@ -136,6 +136,10 @@ func (s *Server) Start() error {
r.POST("/admin/user", IsLogin(), api.AdminSubmitUser) r.POST("/admin/user", IsLogin(), api.AdminSubmitUser)
r.GET("/admin/user/all", IsLogin(), api.AdminGetUsers) 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 // other apis
r.GET("/config", GetConfig) r.GET("/config", GetConfig)
r.GET("/navbars", GetNavbars) r.GET("/navbars", GetNavbars)
@ -144,6 +148,7 @@ func (s *Server) Start() error {
r.GET("/sidebars", GetSidebars) r.GET("/sidebars", GetSidebars)
r.POST("/sidebar", IsLogin(), SubmitSidebar) r.POST("/sidebar", IsLogin(), SubmitSidebar)
err := router.Run(config.Data.Server.Addr) err := router.Run(config.Data.Server.Addr)
if err != nil { if err != nil {
logger.Crit("start backend server error", "error", err) logger.Crit("start backend server error", "error", err)

@ -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) err = db.Conn.QueryRow("select story_id from comments where id=?", c.TargetID).Scan(&storyID)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
logger.Warn("select comment error", "error", err) logger.Warn("select comment error", "error", err)
} else { return e.New(http.StatusInternalServerError, e.Internal)
if storyID == "" { }
storyID = c.TargetID
}
var nid string if storyID == "" {
err := db.Conn.QueryRow("SELECT story_id FROM comments_count WHERE story_id=?", storyID).Scan(&nid) storyID = c.TargetID
if err != nil && err != sql.ErrNoRows { }
logger.Warn("select from comments_count error", "error", err)
return nil
}
if err == sql.ErrNoRows { var nid string
_, err := db.Conn.Exec("INSERT INTO comments_count (story_id,count) VALUES(?,?)", storyID, 1) err = db.Conn.QueryRow("SELECT story_id FROM comments_count WHERE story_id=?", storyID).Scan(&nid)
if err != nil { if err != nil && err != sql.ErrNoRows {
logger.Warn("insert into comments_count error", "error", err) logger.Warn("select from comments_count error", "error", err)
} return nil
} else { }
_, err := db.Conn.Exec("UPDATE comments_count SET count=count+1 WHERE story_id=?", storyID)
if err != nil { if err == sql.ErrNoRows {
logger.Warn("update comments_count error", "error", err) _, 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 creator != "" && creator != c.CreatorID {
if models.GetIDType(c.TargetID) == models.IDTypeComment { if models.GetIDType(c.TargetID) == models.IDTypeComment {
// reply // reply
notification.Send(creator, owner, models.NotificationReply, c.TargetID, c.CreatorID) notification.Send(creator, owner, models.NotificationReply, storyID, c.CreatorID)
} else { } else {
// comment // comment
notification.Send(creator, owner, models.NotificationComment, c.TargetID, c.CreatorID) notification.Send(creator, owner, models.NotificationComment, storyID, c.CreatorID)
} }
} }

@ -2,7 +2,6 @@ package story
import ( import (
"database/sql" "database/sql"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@ -154,7 +153,6 @@ func SubmitStory(c *gin.Context) (map[string]string, *e.Error) {
if post.OwnerID != "" { if post.OwnerID != "" {
followers, err1 = interaction.GetFollowerIDs(post.OwnerID) followers, err1 = interaction.GetFollowerIDs(post.OwnerID)
if err1 == nil { if err1 == nil {
fmt.Println(followers)
for _, f := range followers { for _, f := range followers {
notification.Send("", f, models.NotificationFollow, post.ID, post.CreatorID) notification.Send("", f, models.NotificationFollow, post.ID, post.CreatorID)
} }

@ -1,6 +1,10 @@
package models package models
import "time" import (
"time"
"github.com/imdotdev/im.dev/server/pkg/db"
)
type Comment struct { type Comment struct {
ID string `json:"id"` 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 { func (ar FavorComments) Less(i, j int) bool {
return ar[i].Likes > ar[j].Likes 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
}

@ -1,5 +1,7 @@
package models package models
import "time"
const ( const (
NotificationComment = 1 NotificationComment = 1
NotificationLike = 2 NotificationLike = 2
@ -8,3 +10,13 @@ const (
NotificationFollow = 5 NotificationFollow = 5
NotificationReply = 6 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"`
}

@ -108,3 +108,9 @@ func GetStoryCreatorAndOrg(storyID string) (string, string) {
return creator, owner 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
}

@ -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 (
<>
<Link
aria-label="View notifications"
href={ReserveUrls.Notifications}
>
<HStack cursor="pointer">
<IconButton
size="md"
fontSize="1.4rem"
aria-label="view notifications"
variant="ghost"
color="current"
_focus={null}
icon={<FaBell />}
/>
{unread !== 0 && <Text fontSize=".9rem" pos="absolute" pl="25px" pb="18px" color="orange" fontWeight="bold">{unread}</Text>}
</HStack>
</Link>
</>
)
}
export default Notification

@ -58,6 +58,15 @@ export function getSvgIcon(name, height = "1.4rem") {
case "close": case "close":
svg = <svg height={height} fill="currentColor" viewBox="0 0 320 512"><path d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"></path></svg> svg = <svg height={height} fill="currentColor" viewBox="0 0 320 512"><path d="M193.94 256L296.5 153.44l21.15-21.15c3.12-3.12 3.12-8.19 0-11.31l-22.63-22.63c-3.12-3.12-8.19-3.12-11.31 0L160 222.06 36.29 98.34c-3.12-3.12-8.19-3.12-11.31 0L2.34 120.97c-3.12 3.12-3.12 8.19 0 11.31L126.06 256 2.34 379.71c-3.12 3.12-3.12 8.19 0 11.31l22.63 22.63c3.12 3.12 8.19 3.12 11.31 0L160 289.94 262.56 392.5l21.15 21.15c3.12 3.12 8.19 3.12 11.31 0l22.63-22.63c3.12-3.12 3.12-8.19 0-11.31L193.94 256z"></path></svg>
break break
case "bell":
svg = <svg height={height} viewBox="0 0 448 512"><path d="M224 480c-17.66 0-32-14.38-32-32.03h-32c0 35.31 28.72 64.03 64 64.03s64-28.72 64-64.03h-32c0 17.65-14.34 32.03-32 32.03zm209.38-145.19c-27.96-26.62-49.34-54.48-49.34-148.91 0-79.59-63.39-144.5-144.04-152.35V16c0-8.84-7.16-16-16-16s-16 7.16-16 16v17.56C127.35 41.41 63.96 106.31 63.96 185.9c0 94.42-21.39 122.29-49.35 148.91-13.97 13.3-18.38 33.41-11.25 51.23C10.64 404.24 28.16 416 48 416h352c19.84 0 37.36-11.77 44.64-29.97 7.13-17.82 2.71-37.92-11.26-51.22zM400 384H48c-14.23 0-21.34-16.47-11.32-26.01 34.86-33.19 59.28-70.34 59.28-172.08C95.96 118.53 153.23 64 224 64c70.76 0 128.04 54.52 128.04 121.9 0 101.35 24.21 138.7 59.28 172.08C421.38 367.57 414.17 384 400 384z"></path></svg>
break
case "at":
svg = <svg height={height} fill="currentColor" viewBox="0 0 512 512"><path d="M256 8C118.941 8 8 118.919 8 256c0 137.058 110.919 248 248 248 52.925 0 104.68-17.078 147.092-48.319 5.501-4.052 6.423-11.924 2.095-17.211l-5.074-6.198c-4.018-4.909-11.193-5.883-16.307-2.129C346.93 457.208 301.974 472 256 472c-119.373 0-216-96.607-216-216 0-119.375 96.607-216 216-216 118.445 0 216 80.024 216 200 0 72.873-52.819 108.241-116.065 108.241-19.734 0-23.695-10.816-19.503-33.868l32.07-164.071c1.449-7.411-4.226-14.302-11.777-14.302h-12.421a12 12 0 00-11.781 9.718c-2.294 11.846-2.86 13.464-3.861 25.647-11.729-27.078-38.639-43.023-73.375-43.023-68.044 0-133.176 62.95-133.176 157.027 0 61.587 33.915 98.354 90.723 98.354 39.729 0 70.601-24.278 86.633-46.982-1.211 27.786 17.455 42.213 45.975 42.213C453.089 378.954 504 321.729 504 240 504 103.814 393.863 8 256 8zm-37.92 342.627c-36.681 0-58.58-25.108-58.58-67.166 0-74.69 50.765-121.545 97.217-121.545 38.857 0 58.102 27.79 58.102 65.735 0 58.133-38.369 122.976-96.739 122.976z"></path></svg>
break
case "follow":
svg = <svg height={height} fill="currentColor" viewBox="0 0 512 512"><path d="M496.656 285.683C506.583 272.809 512 256 512 235.468c-.001-37.674-32.073-72.571-72.727-72.571h-70.15c8.72-17.368 20.695-38.911 20.695-69.817C389.819 34.672 366.518 0 306.91 0c-29.995 0-41.126 37.918-46.829 67.228-3.407 17.511-6.626 34.052-16.525 43.951C219.986 134.75 184 192 162.382 203.625c-2.189.922-4.986 1.648-8.032 2.223C148.577 197.484 138.931 192 128 192H32c-17.673 0-32 14.327-32 32v256c0 17.673 14.327 32 32 32h96c17.673 0 32-14.327 32-32v-8.74c32.495 0 100.687 40.747 177.455 40.726 5.505.003 37.65.03 41.013 0 59.282.014 92.255-35.887 90.335-89.793 15.127-17.727 22.539-43.337 18.225-67.105 12.456-19.526 15.126-47.07 9.628-69.405zM32 480V224h96v256H32zm424.017-203.648C472 288 472 336 450.41 347.017c13.522 22.76 1.352 53.216-15.015 61.996 8.293 52.54-18.961 70.606-57.212 70.974-3.312.03-37.247 0-40.727 0-72.929 0-134.742-40.727-177.455-40.727V235.625c37.708 0 72.305-67.939 106.183-101.818 30.545-30.545 20.363-81.454 40.727-101.817 50.909 0 50.909 35.517 50.909 61.091 0 42.189-30.545 61.09-30.545 101.817h111.999c22.73 0 40.627 20.364 40.727 40.727.099 20.363-8.001 36.375-23.984 40.727zM104 432c0 13.255-10.745 24-24 24s-24-10.745-24-24 10.745-24 24-24 24 10.745 24 24z"></path></svg>
break
default: default:
break; break;
} }

@ -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
}
Loading…
Cancel
Save