pull/50/head
sunface 4 years ago
parent a53ffd4fde
commit 4d03dd45b9

@ -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": {

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

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

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

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

@ -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<typeof chakra.div> & {
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 (
<>
{/* <MdEditor
height="100%"
width="100%"
value={props.md}
style={{ height: "102%"}}
renderHTML={_ => null}
onChange={handleEditorChange}
config={{
canView: false,
view:{
menu: props.menu ?? true,
md: true,
html: false,
fullScreen: true,
}
}}
/> */}
<CaretStyles />
<textarea
ref={triggerRef}
placeholder="Type the @ symbol to trigger UI"
spellCheck="false"
onKeyUp={handleCustomUI}
onInput={() => getPositionTrigger(triggerRef)}
/>
<span
className="marker marker--trigger"
style={{
display: showTrigger ? 'block' : 'none',
//@ts-ignore
'--y': triggerY,
'--x': triggerX,
}}>
Triggered UI! <span role="img">😎</span>
</span>
<Popover isOpen={isOpen} closeOnBlur={false} placement="bottom-start" onOpen={onOpen} onClose={onClose} autoFocus={false}>
<Textarea
ref={triggerRef}
placeholder="Type the @ symbol to trigger UI"
spellCheck="false"
onInput={() => getPositionTrigger(triggerRef)}
height="100%"
border={null}
value={props.md}
onChange={handleEditorChange}
_focus={null}
/>
<span
className="marker marker--trigger"
style={{
display: showTrigger ? 'block' : 'none',
//@ts-ignore
'--y': triggerY,
'--x': triggerX,
}}>
{atUsers.length > 0 && <Card role="img" p="0">
<VStack>
{
atUsers.map(user =>
<HStack key={user.id} py="3" px="5" cursor="pointer" onClick={() => selectAtUser(user)} _hover={{bg: bg}}>
<Avatar src={user.avatar} size="sm"/>
<VStack alignItems="left">
{user.nickname !== '' && <Heading size="sm">{user.nickname}</Heading>}
<Text>{user.username}</Text>
</VStack>
</HStack>)
}
</VStack>
</Card>}
</span>
{/* <Popover isOpen={isOpen} closeOnBlur={false} placement="bottom-start" onOpen={onOpen} onClose={onClose} autoFocus={false}>
<PopoverTrigger><Box width="100%"></Box></PopoverTrigger>
<PopoverContent width="100%" transform="translate3d(176px, 2071px, 0px)">
<PopoverBody width="100%" p="0">
ssssss
</PopoverBody>
</PopoverContent>
</Popover>
</>
</Popover> */}
</>
);
}

@ -45,10 +45,8 @@ export function MarkdownRender({ md,fontSize, ...rest }:Props) {
if (username !== '') {
setRenderMd(md.replace('@' + username, `[@${username}](/${username})`))
console.log(username)
}
}
console.log(indexes)
}, [md]);
return (

@ -4,7 +4,7 @@ export function getUserName(user:User) {
}
export function isUsernameChar(c) {
if ((c >= "a" && "c<=z") || (c >= "0" && c <= "9") || (c === "-")) {
if ((c >= "A" && "c<=Z") || (c >= "a" && "c<=z") || (c >= "0" && c <= "9") || (c === "-")) {
return true
}

@ -26,8 +26,6 @@ const CaretStyles = () => (
position: absolute;
left: calc(var(--x, 0) * 1px);
top: calc(var(--y, 0) * 1px);
background: hsl(0, 0%, 10%);
color: hsl(0, 0%, 98%);
z-index: 9999;
padding: 6px;
border-radius: 4px;

@ -1657,7 +1657,7 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
classnames@2.2.6, classnames@^2.2.6:
classnames@2.2.6:
version "2.2.6"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce"
integrity sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==
@ -3522,13 +3522,6 @@ react-is@16.13.1, react-is@^16.7.0, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-markdown-editor-lite-sunface@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/react-markdown-editor-lite-sunface/-/react-markdown-editor-lite-sunface-1.2.5.tgz#22684365e12884990da68aea2903e221de7ffd4c"
integrity sha512-fCQ0RCSZgni5ky43DbZqL6pSlDR7dMkSekTHG5GLRqiOk1jiIsRvvpVjdSMw3HBaworCFRrYSR7oN6WYRg3Dkg==
dependencies:
classnames "^2.2.6"
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"

Loading…
Cancel
Save