diff --git a/package.json b/package.json index e7da03b6..049ff641 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-icons": "^4.1.0", - "react-markdown-editor-lite-sunface": "^1.2.5", "validator": "^13.5.2" }, "devDependencies": { diff --git a/server/internal/api/users.go b/server/internal/api/users.go new file mode 100644 index 00000000..02353956 --- /dev/null +++ b/server/internal/api/users.go @@ -0,0 +1,20 @@ +package api + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/imdotdev/im.dev/server/internal/session" + "github.com/imdotdev/im.dev/server/pkg/common" +) + +func GetUsers(c *gin.Context) { + query := c.Query("query") + users, err := session.GetUsers(query) + if err != nil { + c.JSON(err.Status, common.RespError(err.Message)) + return + } + + c.JSON(http.StatusOK, common.RespSuccess(users)) +} diff --git a/server/internal/cache/cache.go b/server/internal/cache/cache.go new file mode 100644 index 00000000..913949cd --- /dev/null +++ b/server/internal/cache/cache.go @@ -0,0 +1,39 @@ +package cache + +import ( + "time" + + "github.com/imdotdev/im.dev/server/pkg/db" + "github.com/imdotdev/im.dev/server/pkg/log" + "github.com/imdotdev/im.dev/server/pkg/models" +) + +var logger = log.RootLogger.New("logger", "cache") +var Users []*models.User + +func Init() { + for { + // load users + rows, err := db.Conn.Query(`SELECT id,username,role,nickname,email,avatar,last_seen_at,created FROM user`) + if err != nil { + logger.Error("load users error", "error", err) + time.Sleep(60 * time.Second) + continue + } + + var users []*models.User + for rows.Next() { + user := &models.User{} + err := rows.Scan(&user.ID, &user.Username, &user.Role, &user.Nickname, &user.Email, &user.Avatar, &user.LastSeenAt, &user.Created) + if err != nil { + logger.Warn("scan user error", "error", err) + continue + } + users = append(users, user) + } + + Users = users + + time.Sleep(60 * time.Second) + } +} diff --git a/server/internal/server.go b/server/internal/server.go index d0a1d5aa..f80942e7 100644 --- a/server/internal/server.go +++ b/server/internal/server.go @@ -5,6 +5,7 @@ import ( "github.com/gin-gonic/gin" "github.com/imdotdev/im.dev/server/internal/api" + "github.com/imdotdev/im.dev/server/internal/cache" "github.com/imdotdev/im.dev/server/internal/session" "github.com/imdotdev/im.dev/server/internal/storage" "github.com/imdotdev/im.dev/server/pkg/common" @@ -36,6 +37,7 @@ func (s *Server) Start() error { gin.SetMode(gin.DebugMode) } + go cache.Init() go func() { router := gin.New() router.Use(Cors()) @@ -50,22 +52,23 @@ func (s *Server) Start() error { r.GET("/post/:id", api.GetPost) - r.POST("/story/like/:id", api.LikeStory, IsLogin()) + r.POST("/story/like/:id", IsLogin(), api.LikeStory) r.GET("/story/comments/:id", api.GetStoryComments) - r.POST("/story/comment", api.SubmitComment, IsLogin()) - r.DELETE("/comment/:id", api.DeleteComment, IsLogin()) + r.POST("/story/comment", IsLogin(), api.SubmitComment) + r.DELETE("/comment/:id", IsLogin(), api.DeleteComment) - r.GET("/editor/posts", api.GetEditorPosts, IsLogin()) - r.POST("/editor/post", api.SubmitPost, IsLogin()) - r.DELETE("/editor/post/:id", api.DeletePost, IsLogin()) - r.GET("/editor/post/:id", api.GetEditorPost, IsLogin()) + r.GET("/editor/posts", IsLogin(), api.GetEditorPosts) + r.POST("/editor/post", IsLogin(), api.SubmitPost) + r.DELETE("/editor/post/:id", IsLogin(), api.DeletePost) + r.GET("/editor/post/:id", IsLogin(), api.GetEditorPost) - r.POST("/admin/tag", api.SubmitTag, IsLogin()) - r.DELETE("/admin/tag/:id", api.DeleteTag, IsLogin()) + r.POST("/admin/tag", IsLogin(), api.SubmitTag) + r.DELETE("/admin/tag/:id", IsLogin(), api.DeleteTag) r.GET("/tags", api.GetTags) r.GET("/tag/:name", api.GetTag) + r.GET("/users", api.GetUsers) err := router.Run(config.Data.Server.Addr) if err != nil { logger.Crit("start backend server error", "error", err) diff --git a/server/internal/session/users.go b/server/internal/session/users.go new file mode 100644 index 00000000..77169b61 --- /dev/null +++ b/server/internal/session/users.go @@ -0,0 +1,28 @@ +package session + +import ( + "strings" + + "github.com/imdotdev/im.dev/server/internal/cache" + "github.com/imdotdev/im.dev/server/pkg/e" + "github.com/imdotdev/im.dev/server/pkg/models" +) + +func GetUsers(q string) ([]*models.User, *e.Error) { + allUsers := cache.Users + + users := make([]*models.User, 0) + for _, u := range allUsers { + if strings.HasPrefix(strings.ToLower(u.Nickname), strings.ToLower(q)) { + users = append(users, u) + continue + } + + if strings.HasPrefix(strings.ToLower(u.Username), strings.ToLower(q)) { + users = append(users, u) + continue + } + } + + return users, nil +} diff --git a/src/components/markdown-editor/editor.tsx b/src/components/markdown-editor/editor.tsx index b4258d29..54cfd2fd 100644 --- a/src/components/markdown-editor/editor.tsx +++ b/src/components/markdown-editor/editor.tsx @@ -1,15 +1,16 @@ /*eslint-disable*/ -import React, { useRef, useEffect,useState} from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import 'highlight.js/styles/atom-one-dark.css'; -import { chakra,Popover,PopoverTrigger,PopoverContent,PopoverBody,Box,PropsOf, useDisclosure} from '@chakra-ui/react'; +import { chakra, Popover, PopoverTrigger, PopoverContent, PopoverBody, Box, PropsOf, useDisclosure, Textarea, VStack, HStack, Avatar, Heading, Text, useColorModeValue } from '@chakra-ui/react'; -import dynamic from 'next/dynamic'; -import 'react-markdown-editor-lite-sunface/lib/index.css'; import useCaretPosition from './position' import CaretStyles from 'theme/caret.styles' -const MdEditor = dynamic(() => import('node_modules/react-markdown-editor-lite-sunface'), { - ssr: false -}); +import { isUsernameChar } from 'utils/user'; +import { requestApi } from 'utils/axios/request'; +import Card from 'components/card'; +import { User } from 'src/types/session'; +import userCustomTheme from 'theme/user-custom'; +import { cloneDeep } from 'lodash'; @@ -21,14 +22,11 @@ type Props = PropsOf & { -export function MarkdownEditor(props:Props) { - const { onOpen, onClose, isOpen } = useDisclosure() +export function MarkdownEditor(props: Props) { + const bg = useColorModeValue(userCustomTheme.hoverBg.light,userCustomTheme.hoverBg.dark) + const [at,setAt] = useState('') + const [atUsers,setAtUsers]:[User[],any] = useState([]) - function handleEditorChange({html, text}) { - props.onChange(text) - onOpen() - } - const triggerRef = useRef(null) const [showTrigger, setShowTrigger] = useState(false) const { @@ -37,74 +35,127 @@ export function MarkdownEditor(props:Props) { getPosition: getPositionTrigger, } = useCaretPosition(triggerRef) - const handleCustomUI = (e) => { - const previousCharacter = e.target.value - .charAt(triggerRef.current.selectionStart - 2) - .trim() - const character = e.target.value - .charAt(triggerRef.current.selectionStart - 1) - .trim() - if (character === '@' && previousCharacter === '') { - setShowTrigger(true) - } - if (character === '' && showTrigger) { - setShowTrigger(false) - } - } - useEffect(() => { if (triggerRef.current) { getPositionTrigger(triggerRef) } }, []) + useEffect(() => { + if (at !== '') { + requestApi.get(`/users?query=${at.trim()}`).then(res => setAtUsers(res.data)) + } + },[at]) + function handleEditorChange(e) { + handleAt(e) + props.onChange(e.currentTarget.value) + } + + + const handleAt = (e) => { + // 当输入时,找到上一个@的位置,切之前的字母都必须是合法的username字符 + // 若找到,则记录该字符串,同时设置showTrigger = true + let at0 = '' + let show = false + for(let i=triggerRef.current.selectionStart; i--; i >= 0) { + if (e.target.value.charAt(i) === '@') { + show = true + break + } + + if (!isUsernameChar(e.target.value.charAt(i))) { + show = false + break + } + + at0 = e.target.value.charAt(i) + at0 + } + + if (show) { + if (at !== at0) { + setAt(at0) + } + setShowTrigger(true) + } else { + setShowTrigger(false) + setAt('') + } + } + + + + const selectAtUser = (user:User) => { + const md = cloneDeep(props.md) + + let end = triggerRef.current.selectionStart + let start: number + for(let i=end; i--; i >= 0) { + if (md.charAt(i) === '@') { + start = i + break + } + } + + const newMd = md.substr(0,start) + '@'+user.username+ md.substr(end,md.length) + props.onChange(newMd) + setShowTrigger(false) + + const gap = user.username.length - at.length + setAt('') + setTimeout(() => { + triggerRef.current.selectionStart = end + gap + triggerRef.current.selectionEnd = end + gap + triggerRef.current.focus() + },0) +} + return ( <> - {/* null} - onChange={handleEditorChange} - config={{ - canView: false, - view:{ - menu: props.menu ?? true, - md: true, - html: false, - fullScreen: true, - } - }} - /> */} -