12 changed files with 387 additions and 15 deletions
@ -0,0 +1,231 @@ |
|||||||
|
import React, { useState, useEffect } from "react"; |
||||||
|
import { Table, Typography, Tooltip, Button } from "antd"; |
||||||
|
import { CheckCircleFilled, ExclamationCircleFilled, StopOutlined } from "@ant-design/icons"; |
||||||
|
import classNames from 'classnames'; |
||||||
|
import { ColumnsType } from 'antd/es/table'; |
||||||
|
import format from 'date-fns/format' |
||||||
|
|
||||||
|
import { CHAT_HISTORY, fetchData, UPDATE_CHAT_MESSGAE_VIZ } from "../utils/apis"; |
||||||
|
import { MessageType } from '../types/chat'; |
||||||
|
import { isEmptyObject } from "../utils/format"; |
||||||
|
|
||||||
|
const { Title } = Typography; |
||||||
|
|
||||||
|
function createUserNameFilters(messages: MessageType[]) { |
||||||
|
const filtered = messages.reduce((acc, curItem) => { |
||||||
|
const curAuthor = curItem.author; |
||||||
|
if (!acc.some(item => item.text === curAuthor)) { |
||||||
|
acc.push({ text: curAuthor, value: curAuthor }); |
||||||
|
} |
||||||
|
return acc; |
||||||
|
}, []); |
||||||
|
|
||||||
|
// sort by name
|
||||||
|
return filtered.sort((a, b) => { |
||||||
|
const nameA = a.text.toUpperCase(); // ignore upper and lowercase
|
||||||
|
const nameB = b.text.toUpperCase(); // ignore upper and lowercase
|
||||||
|
if (nameA < nameB) { |
||||||
|
return -1; |
||||||
|
} |
||||||
|
if (nameA > nameB) { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
// names must be equal
|
||||||
|
return 0; |
||||||
|
}); |
||||||
|
} |
||||||
|
export const OUTCOME_TIMEOUT = 3000; |
||||||
|
|
||||||
|
export default function Chat() { |
||||||
|
const [messages, setMessages] = useState([]); |
||||||
|
const [selectedRowKeys, setSelectedRows] = useState([]); |
||||||
|
const [bulkProcessing, setBulkProcessing] = useState(false); |
||||||
|
const [bulkOutcome, setBulkOutcome] = useState(null); |
||||||
|
const [bulkAction, setBulkAction] = useState(''); |
||||||
|
let outcomeTimeout = null; |
||||||
|
|
||||||
|
const getInfo = async () => { |
||||||
|
try { |
||||||
|
const result = await fetchData(CHAT_HISTORY, { auth: true }); |
||||||
|
if (isEmptyObject(result)) { |
||||||
|
setMessages([]); |
||||||
|
} else { |
||||||
|
setMessages(result); |
||||||
|
} |
||||||
|
} catch (error) { |
||||||
|
console.log("==== error", error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
getInfo(); |
||||||
|
return () => { |
||||||
|
clearTimeout(outcomeTimeout); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
const nameFilters = createUserNameFilters(messages); |
||||||
|
|
||||||
|
const rowSelection = { |
||||||
|
selectedRowKeys, |
||||||
|
onChange: (selectedKeys: string[]) => { |
||||||
|
setSelectedRows(selectedKeys); |
||||||
|
}, |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
const resetBulkOutcome = () => { |
||||||
|
outcomeTimeout = setTimeout(() => { |
||||||
|
setBulkOutcome(null); |
||||||
|
setBulkAction(''); |
||||||
|
}, OUTCOME_TIMEOUT); |
||||||
|
}; |
||||||
|
const handleSubmitBulk = async (bulkVisibility) => { |
||||||
|
setBulkProcessing(true); |
||||||
|
const result = await fetchData(UPDATE_CHAT_MESSGAE_VIZ, { |
||||||
|
auth: true, |
||||||
|
method: 'POST', |
||||||
|
data: { |
||||||
|
visible: bulkVisibility, |
||||||
|
idArray: selectedRowKeys, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
if (result.success && result.message === "changed") { |
||||||
|
setBulkOutcome(<CheckCircleFilled />); |
||||||
|
resetBulkOutcome(); |
||||||
|
|
||||||
|
// update messages
|
||||||
|
const updatedList = [...messages]; |
||||||
|
selectedRowKeys.map(key => { |
||||||
|
const messageIndex = updatedList.findIndex(m => m.id === key); |
||||||
|
const newMessage = {...messages[messageIndex], visible: bulkVisibility }; |
||||||
|
updatedList.splice(messageIndex, 1, newMessage); |
||||||
|
return null; |
||||||
|
}); |
||||||
|
setMessages(updatedList); |
||||||
|
setSelectedRows([]); |
||||||
|
} else { |
||||||
|
setBulkOutcome(<ExclamationCircleFilled />); |
||||||
|
resetBulkOutcome(); |
||||||
|
} |
||||||
|
setBulkProcessing(false); |
||||||
|
} |
||||||
|
const handleSubmitBulkShow = () => { |
||||||
|
setBulkAction('show'); |
||||||
|
handleSubmitBulk(true); |
||||||
|
} |
||||||
|
const handleSubmitBulkHide = () => { |
||||||
|
setBulkAction('hide'); |
||||||
|
handleSubmitBulk(false); |
||||||
|
} |
||||||
|
|
||||||
|
const chatColumns: ColumnsType<MessageType> = [ |
||||||
|
{ |
||||||
|
title: 'Time', |
||||||
|
dataIndex: 'timestamp', |
||||||
|
key: 'timestamp', |
||||||
|
className: 'timestamp-col', |
||||||
|
defaultSortOrder: 'descend', |
||||||
|
render: (timestamp) => { |
||||||
|
const dateObject = new Date(timestamp); |
||||||
|
return format(dateObject, 'PP pp'); |
||||||
|
}, |
||||||
|
sorter: (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime(), |
||||||
|
width: 90, |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'User', |
||||||
|
dataIndex: 'author', |
||||||
|
key: 'author', |
||||||
|
className: 'name-col', |
||||||
|
filters: nameFilters, |
||||||
|
onFilter: (value, record) => record.author === value, |
||||||
|
sorter: (a, b) => a.author.localeCompare(b.author), |
||||||
|
sortDirections: ['ascend', 'descend'], |
||||||
|
ellipsis: true, |
||||||
|
render: author => ( |
||||||
|
<Tooltip placement="topLeft" title={author}> |
||||||
|
{author} |
||||||
|
</Tooltip> |
||||||
|
), |
||||||
|
width: 110, |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: 'Message', |
||||||
|
dataIndex: 'body', |
||||||
|
key: 'body', |
||||||
|
className: 'message-col', |
||||||
|
width: 320, |
||||||
|
render: body => ( |
||||||
|
<div |
||||||
|
className="message-contents" |
||||||
|
// eslint-disable-next-line react/no-danger
|
||||||
|
dangerouslySetInnerHTML={{ __html: body }} |
||||||
|
/> |
||||||
|
) |
||||||
|
}, |
||||||
|
{ |
||||||
|
title: '', |
||||||
|
dataIndex: 'visible', |
||||||
|
key: 'visible', |
||||||
|
className: 'toggle-col', |
||||||
|
filters: [{ text: 'Visible messages', value: true }, { text: 'Hidden messages', value: false }], |
||||||
|
onFilter: (value, record) => record.visible === value, |
||||||
|
render: visible => visible ? null : <StopOutlined title="This message is hidden" />, |
||||||
|
width: 30, |
||||||
|
}, |
||||||
|
]; |
||||||
|
|
||||||
|
const bulkDivClasses = classNames({ |
||||||
|
'bulk-editor': true, |
||||||
|
active: selectedRowKeys.length, |
||||||
|
}); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="chat-messages"> |
||||||
|
<Title level={2}>Chat Messages</Title> |
||||||
|
<p>Manage the messages from viewers that show up on your stream.</p> |
||||||
|
<div className={bulkDivClasses}> |
||||||
|
<span className="label">Check multiple messages to change their visibility to: </span> |
||||||
|
|
||||||
|
<Button |
||||||
|
type="primary" |
||||||
|
size="small" |
||||||
|
shape="round" |
||||||
|
className="button" |
||||||
|
loading={bulkAction === 'show' && bulkProcessing} |
||||||
|
icon={bulkAction === 'show' && bulkOutcome} |
||||||
|
disabled={!selectedRowKeys.length || (bulkAction && bulkAction !== 'show')} |
||||||
|
onClick={handleSubmitBulkShow} |
||||||
|
> |
||||||
|
Show |
||||||
|
</Button> |
||||||
|
<Button |
||||||
|
type="primary" |
||||||
|
size="small" |
||||||
|
shape="round" |
||||||
|
className="button" |
||||||
|
loading={bulkAction === 'hide' && bulkProcessing} |
||||||
|
icon={bulkAction === 'hide' && bulkOutcome} |
||||||
|
disabled={!selectedRowKeys.length || (bulkAction && bulkAction !== 'hide')} |
||||||
|
onClick={handleSubmitBulkHide} |
||||||
|
> |
||||||
|
Hide |
||||||
|
</Button> |
||||||
|
</div> |
||||||
|
<Table |
||||||
|
size="small" |
||||||
|
className="messages-table" |
||||||
|
pagination={{ pageSize: 100 }} |
||||||
|
scroll={{ y: 540 }} |
||||||
|
rowClassName={record => !record.visible ? 'hidden' : ''} |
||||||
|
dataSource={messages} |
||||||
|
columns={chatColumns} |
||||||
|
rowKey={(row) => row.id} |
||||||
|
rowSelection={rowSelection} |
||||||
|
/> |
||||||
|
</div>) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,83 @@ |
|||||||
|
.chat-messages { |
||||||
|
.ant-table-small .ant-table-selection-column { |
||||||
|
width: 20px; |
||||||
|
min-width: 20px; |
||||||
|
} |
||||||
|
.ant-table-tbody > tr > td { |
||||||
|
transition: background 0.15s; |
||||||
|
} |
||||||
|
.ant-table-row.hidden { |
||||||
|
.ant-table-cell { |
||||||
|
color: #444450; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
.ant-table-cell { |
||||||
|
font-size: 12px; |
||||||
|
|
||||||
|
&.name-col { |
||||||
|
text-overflow: ellipsis; |
||||||
|
overflow: hidden; |
||||||
|
} |
||||||
|
&.toggle-col { |
||||||
|
label { |
||||||
|
font-size: 11px; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.message-contents { |
||||||
|
overflow: auto; |
||||||
|
max-height: 200px; |
||||||
|
img { |
||||||
|
position: relative; |
||||||
|
margin-top: -5px; |
||||||
|
width: 3rem; |
||||||
|
padding: 0.25rem; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.bulk-editor { |
||||||
|
margin: .5rem 0; |
||||||
|
padding: .5rem; |
||||||
|
border: 1px solid #333; |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
justify-content: flex-end; |
||||||
|
border-radius: 4px; |
||||||
|
|
||||||
|
&.active { |
||||||
|
.label { |
||||||
|
color: #ccc; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
.label { |
||||||
|
font-size: .75rem; |
||||||
|
color: #666; |
||||||
|
margin-right: .5rem; |
||||||
|
} |
||||||
|
|
||||||
|
button { |
||||||
|
margin: 0 .2rem; |
||||||
|
font-size: .75rem; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
.ant-table-filter-dropdown { |
||||||
|
max-width: 250px; |
||||||
|
} |
||||||
|
|
||||||
|
.toggle-switch { |
||||||
|
display: flex; |
||||||
|
flex-direction: row; |
||||||
|
align-items: center; |
||||||
|
flex-wrap: nowrap; |
||||||
|
justify-content: flex-end; |
||||||
|
|
||||||
|
.outcome-icon { |
||||||
|
margin-right: .5rem; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
export interface MessageType { |
||||||
|
author: string; |
||||||
|
body: string; |
||||||
|
id: string; |
||||||
|
key: string; |
||||||
|
name: string; |
||||||
|
timestamp: string; |
||||||
|
type: string; |
||||||
|
visible: boolean; |
||||||
|
} |
||||||
Loading…
Reference in new issue