From 26a3c76f3f00dea99dee34289f979696291319a9 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sun, 16 Apr 2023 12:50:01 +0800 Subject: [PATCH] feat: able to view messages on dashboard (close #32) --- common/constants.go | 7 + controller/message.go | 33 +++ model/message.go | 14 +- router/api-router.go | 5 +- web/src/App.js | 9 + web/src/components/Header.js | 5 + web/src/components/MessagesTable.js | 342 ++++++++++++++++++++++++++++ web/src/pages/Message/index.js | 14 ++ 8 files changed, 426 insertions(+), 3 deletions(-) create mode 100644 web/src/components/MessagesTable.js create mode 100644 web/src/pages/Message/index.js diff --git a/common/constants.go b/common/constants.go index 0d683a3..ddff2f7 100644 --- a/common/constants.go +++ b/common/constants.go @@ -92,3 +92,10 @@ const ( SendEmailToOthersAllowed = 1 SendEmailToOthersDisallowed = 2 ) + +const ( + MessageSendStatusUnknown = 0 + MessageSendStatusPending = 1 + MessageSendStatusSent = 2 + MessageSendStatusFailed = 3 +) diff --git a/controller/message.go b/controller/message.go index 3206cda..974ee7e 100644 --- a/controller/message.go +++ b/controller/message.go @@ -134,12 +134,26 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { "success": false, "message": err.Error(), }) + // Update the status of the message + if common.MessagePersistenceEnabled { + err := message.UpdateStatus(common.MessageSendStatusFailed) + if err != nil { + common.SysError("failed to update the status of the message: " + err.Error()) + } + } return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": "ok", }) + // Update the status of the message + if common.MessagePersistenceEnabled { + err := message.UpdateStatus(common.MessageSendStatusSent) + if err != nil { + common.SysError("failed to update the status of the message: " + err.Error()) + } + } return } @@ -235,6 +249,25 @@ func GetMessage(c *gin.Context) { return } +func SearchMessages(c *gin.Context) { + // TODO: improve the search algorithm + keyword := c.Query("keyword") + messages, err := model.SearchMessages(keyword) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": messages, + }) + return +} + func DeleteMessage(c *gin.Context) { messageId, _ := strconv.Atoi(c.Param("id")) userId := c.GetInt("id") diff --git a/model/message.go b/model/message.go index 1917d8f..91ad19b 100644 --- a/model/message.go +++ b/model/message.go @@ -19,7 +19,8 @@ type Message struct { HTMLContent string `json:"html_content" gorm:"-:all"` Timestamp int64 `json:"timestamp" gorm:"type:int64"` Link string `json:"link" gorm:"unique;index"` - To string `json:"to" gorm:"column:to"` // if specified, will send to this user(s) + To string `json:"to" gorm:"column:to"` // if specified, will send to this user(s) + Status int `json:"status" gorm:"default:0"` // pending, sent, failed } func GetMessageById(id int, userId int) (*Message, error) { @@ -45,6 +46,11 @@ func GetMessagesByUserId(userId int, startIdx int, num int) (messages []*Message return messages, err } +func SearchMessages(keyword string) (messages []*Message, err error) { + err = DB.Select([]string{"id", "title", "channel", "status"}).Where("id = ? or title LIKE ? or channel = ? or status = ?", keyword, keyword+"%", keyword, keyword).Find(&messages).Error + return messages, err +} + func DeleteMessageById(id int, userId int) (err error) { // Why we need userId here? In case user want to delete other's message. if id == 0 || userId == 0 { @@ -66,11 +72,17 @@ func (message *Message) UpdateAndInsert(userId int) error { message.Link = common.GetUUID() message.Timestamp = time.Now().Unix() message.UserId = userId + message.Status = common.MessageSendStatusPending var err error err = DB.Create(message).Error return err } +func (message *Message) UpdateStatus(status int) error { + err := DB.Model(message).Update("status", status).Error + return err +} + func (message *Message) Delete() error { err := DB.Delete(message).Error return err diff --git a/router/api-router.go b/router/api-router.go index 5e3643f..b91f79d 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -57,9 +57,10 @@ func SetApiRouter(router *gin.Engine) { } messageRoute := apiRouter.Group("/message") { - messageRoute.GET("/all", middleware.UserAuth(), controller.GetUserMessages) + messageRoute.GET("/", middleware.UserAuth(), controller.GetUserMessages) + messageRoute.GET("/search", middleware.UserAuth(), controller.SearchMessages) messageRoute.GET("/:id", middleware.UserAuth(), controller.GetMessage) - messageRoute.DELETE("/all", middleware.RootAuth(), controller.DeleteAllMessages) + messageRoute.DELETE("/", middleware.RootAuth(), controller.DeleteAllMessages) messageRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteMessage) } } diff --git a/web/src/App.js b/web/src/App.js index ca0908a..c11a444 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -15,6 +15,7 @@ import GitHubOAuth from './components/GitHubOAuth'; import PasswordResetConfirm from './components/PasswordResetConfirm'; import { UserContext } from './context/User'; import { StatusContext } from './context/Status'; +import Message from './pages/Message'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -106,6 +107,14 @@ function App() { } /> + + + + } + /> 邮件; + case 'test': + return ( + + ); + case 'corp_app': + return ( + + ); + case 'corp': + return ( + + ); + case 'lark': + return ( + + ); + case 'ding': + return ( + + ); + case 'bark': + return ( + + ); + case 'client': + return ( + + ); + case 'telegram': + return ( + + ); + case 'discord': + return ( + + ); + case 'none': + return ; + default: + return ; + } +} + +function renderTimestamp(timestamp) { + const date = new Date(timestamp * 1000); + return ( + <> + {date.getFullYear()}-{date.getMonth() + 1}-{date.getDate()}{' '} + {date.getHours()}:{date.getMinutes()}:{date.getSeconds()} + + ); +} + +function renderStatus(status) { + switch (status) { + case 1: + return ( + + ); + case 2: + return ( + + ); + case 3: + return ( + + ); + default: + return ( + + ); + } +} + +const MessagesTable = () => { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + + const loadMessages = async (startIdx) => { + const res = await API.get(`/api/message/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setMessages(data); + } else { + let newMessages = messages; + newMessages.push(...data); + setMessages(newMessages); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(messages.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadMessages(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + useEffect(() => { + // TODO: Prompt the user if message persistence is disabled + // TODO: Allow set persistence permission for each user + loadMessages(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const viewMessage = (id) => { + // TODO: Implement viewMessage + console.log('viewMessage', id); + }; + + const resendMessage = (id) => { + // TODO: Implement resendMessage + console.log('resendMessage', id); + }; + + const deleteMessage = (id) => { + // TODO: Implement deleteMessage + console.log('deleteMessage', id); + }; + + const searchMessages = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadMessages(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/message/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setMessages(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); + }; + + const sortMessage = (key) => { + if (messages.length === 0) return; + setLoading(true); + let sortedMessages = [...messages]; + sortedMessages.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedMessages[0].id === messages[0].id) { + sortedMessages.reverse(); + } + setMessages(sortedMessages); + setLoading(false); + }; + + return ( + <> +
+ + + + + + { + sortMessage('id'); + }} + > + 消息 ID + + { + sortMessage('title'); + }} + > + 标题 + + { + sortMessage('channel'); + }} + > + 通道 + + { + sortMessage('timestamp'); + }} + > + 发送时间 + + { + sortMessage('status'); + }} + > + 状态 + + 操作 + + + + + {messages + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((message, idx) => { + if (message.deleted) return <>; + return ( + + {'#' + message.id} + + {message.title ? message.title : '无标题'} + + {renderChannel(message.channel)} + {renderTimestamp(message.timestamp)} + {renderStatus(message.status)} + +
+ + + +
+
+
+ ); + })} +
+ + + + + + + + +
+ + ); +}; + +export default MessagesTable; diff --git a/web/src/pages/Message/index.js b/web/src/pages/Message/index.js new file mode 100644 index 0000000..2d51ef6 --- /dev/null +++ b/web/src/pages/Message/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Header, Segment } from 'semantic-ui-react'; +import MessagesTable from '../../components/MessagesTable'; + +const Message = () => ( + <> + +
我的消息
+ +
+ +); + +export default Message;