feat: able to view messages on dashboard (close #32)
This commit is contained in:
@@ -92,3 +92,10 @@ const (
|
||||
SendEmailToOthersAllowed = 1
|
||||
SendEmailToOthersDisallowed = 2
|
||||
)
|
||||
|
||||
const (
|
||||
MessageSendStatusUnknown = 0
|
||||
MessageSendStatusPending = 1
|
||||
MessageSendStatusSent = 2
|
||||
MessageSendStatusFailed = 3
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
+13
-1
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/message'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Message />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
|
||||
@@ -13,6 +13,11 @@ const headerButtons = [
|
||||
to: '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
name: '消息',
|
||||
to: '/message',
|
||||
icon: 'mail',
|
||||
},
|
||||
{
|
||||
name: '用户',
|
||||
to: '/user',
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
|
||||
import { API, showError } from '../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
|
||||
function renderChannel(channel) {
|
||||
switch (channel) {
|
||||
case 'email':
|
||||
return <Label color='green'>邮件</Label>;
|
||||
case 'test':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#2cbb00', color: 'white' }}>
|
||||
微信测试号
|
||||
</Label>
|
||||
);
|
||||
case 'corp_app':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#5fc9ec', color: 'white' }}>
|
||||
企业微信应用号
|
||||
</Label>
|
||||
);
|
||||
case 'corp':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#019d82', color: 'white' }}>
|
||||
企业微信群机器人
|
||||
</Label>
|
||||
);
|
||||
case 'lark':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#00d6b9', color: 'white' }}>
|
||||
飞书群机器人
|
||||
</Label>
|
||||
);
|
||||
case 'ding':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#007fff', color: 'white' }}>
|
||||
钉钉群机器人
|
||||
</Label>
|
||||
);
|
||||
case 'bark':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#ff3b30', color: 'white' }}>
|
||||
Bark App
|
||||
</Label>
|
||||
);
|
||||
case 'client':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#121212', color: 'white' }}>
|
||||
WebSocket 客户端
|
||||
</Label>
|
||||
);
|
||||
case 'telegram':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#29a9ea', color: 'white' }}>
|
||||
Telegram 机器人
|
||||
</Label>
|
||||
);
|
||||
case 'discord':
|
||||
return (
|
||||
<Label style={{ backgroundColor: '#404eed', color: 'white' }}>
|
||||
Discord 群机器人
|
||||
</Label>
|
||||
);
|
||||
case 'none':
|
||||
return <Label>无</Label>;
|
||||
default:
|
||||
return <Label color='grey'>未知通道</Label>;
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<Label basic color='olive'>
|
||||
投递中...
|
||||
</Label>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<Label basic color='green'>
|
||||
发送成功
|
||||
</Label>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<Label basic color='red'>
|
||||
发送失败
|
||||
</Label>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<Label basic color='grey'>
|
||||
未知状态
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Form onSubmit={searchMessages}>
|
||||
<Form.Input
|
||||
icon='search'
|
||||
fluid
|
||||
iconPosition='left'
|
||||
placeholder='搜索消息的 ID,标题,通道,以及发送状态 ...'
|
||||
value={searchKeyword}
|
||||
loading={searching}
|
||||
onChange={handleKeywordChange}
|
||||
/>
|
||||
</Form>
|
||||
<Table basic loading={loading}>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortMessage('id');
|
||||
}}
|
||||
>
|
||||
消息 ID
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortMessage('title');
|
||||
}}
|
||||
>
|
||||
标题
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortMessage('channel');
|
||||
}}
|
||||
>
|
||||
通道
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortMessage('timestamp');
|
||||
}}
|
||||
>
|
||||
发送时间
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortMessage('status');
|
||||
}}
|
||||
>
|
||||
状态
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{messages
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((message, idx) => {
|
||||
if (message.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={message.id}>
|
||||
<Table.Cell>{'#' + message.id}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{message.title ? message.title : '无标题'}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderChannel(message.channel)}</Table.Cell>
|
||||
<Table.Cell>{renderTimestamp(message.timestamp)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(message.status)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={() => {
|
||||
viewMessage(message.id);
|
||||
}}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
color={'yellow'}
|
||||
onClick={() => {
|
||||
resendMessage(message.id);
|
||||
}}
|
||||
>
|
||||
重发
|
||||
</Button>
|
||||
<Button
|
||||
size={'small'}
|
||||
negative
|
||||
onClick={() => {
|
||||
deleteMessage(message.id);
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan='6'>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(messages.length / ITEMS_PER_PAGE) +
|
||||
(messages.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MessagesTable;
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import { Header, Segment } from 'semantic-ui-react';
|
||||
import MessagesTable from '../../components/MessagesTable';
|
||||
|
||||
const Message = () => (
|
||||
<>
|
||||
<Segment>
|
||||
<Header as='h3'>我的消息</Header>
|
||||
<MessagesTable />
|
||||
</Segment>
|
||||
</>
|
||||
);
|
||||
|
||||
export default Message;
|
||||
Reference in New Issue
Block a user