13 changed files with 390 additions and 16 deletions
@ -0,0 +1,231 @@
@@ -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 @@
@@ -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 @@
@@ -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