webrtc/src/chat.js

352 lines
15 KiB
JavaScript

import { get, set, del, update, createStore, values } from 'idb-keyval'
import { Span, Button, List, ListItem, TextArea, createElement, Input } from './weigets.js'
export default class Chat {
constructor({ name, EventListeners = {}, onsend, onexit }) {
this.event = { onsend, onexit }
this.store = createStore(`db-chat-${name}`, `store-chat-${name}`)
this.ul = List({ classList: ['chat-list'] })
this.EventListeners = EventListeners
this.element = createElement({
style: {
position: 'fixed', bottom: 0, left: '50%',
maxWidth: '64rem', width: '100%',
transform: 'translate(-50%, 0)',
display: 'block',
transition: 'all .15s',
},
children: [
this.ul,
createElement({
style: {
display: 'flex',
margin: '1rem 2rem',
},
children: [
Input({
type: 'text',
// 随机显示一句话
placeholder: [
'输入聊天内容',
'ctrl+左键: 左边显示',
'ctrl+右键: 右边显示',
'ctrl+下键: 居中显示',
'ctrl+L: 清屏',
].sort(() => Math.random() - 0.5)[0],
style: {
flex: 1,
height: '3rem',
padding: '0 1rem',
boxSizing: 'border-box',
boxShadow: '0 0 1rem #eee',
border: 'none',
outline: 'none',
},
onkeydown: event => {
event.stopPropagation()
const text = event.target.value.trim()
if (text && event.key === 'Enter') {
this.发送消息(text)
event.target.value = ''
}
if (!text && event.key === 'Enter') {
event.target.parentNode.parentNode.style.display = 'none'
}
if (event.ctrlKey && event.key === 'ArrowLeft') {
event.target.parentNode.parentNode.style.left = '0'
event.target.parentNode.parentNode.style.right = '50%'
}
if (event.ctrlKey && event.key === 'ArrowRight') {
event.target.parentNode.parentNode.style.left = '50%'
event.target.parentNode.parentNode.style.right = '0'
}
if (event.ctrlKey && event.key === 'ArrowDown') {
event.target.parentNode.parentNode.style.left = '50%'
event.target.parentNode.parentNode.style.right = '50%'
}
if (event.ctrlKey && event.key === 'l') {
this.ul.innerHTML = '' // 清理屏幕
// 获取当前时间戳
const timestamp = new Date().getTime()
// 清理本地存储
values(this.store).then(list => list.forEach(item => {
const t = new Date(item.time).getTime()
if (t < timestamp) del(item.id, this.store)
}))
console.log('清屏', `store-chat-${name}`, timestamp)
localStorage.setItem(`store-chat-${name}`, timestamp)
}
}
}),
Button({
textContent: '发送(Enter)',
onclick: event => {
const text = event.target.previousSibling.value.trim()
if (text) {
this.发送消息(text)
event.target.previousSibling.value = ''
}
},
style: {
padding: '.5rem 1rem',
boxSizing: 'border-box',
boxShadow: '0 0 1rem #eee',
borderRadius: '1rem',
}
}),
]
})
]
})
document.body.appendChild(this.element)
document.head.appendChild(createElement({
innerText: `
ul.chat-list::-webkit-scrollbar {
display: none;
}
ul.chat-list {
scrollbar-width: none;
}
ul.chat-list {
max-height: 70vh;
overflow-y: auto;
}
ul.chat-list > li > span {
cursor: pointer;
}
ul.chat-list > li.play > span {
color: #02be08;
}
ul.chat-list > li.cache::marker {
color: #02be08;
font-size: 1em;
contentx: '⚡';
}
ul.chat-list > li.disable {
color: #888;
}`
}, 'style'))
this.从本地载入消息()
this.挂载全局快捷键()
}
async 挂载全局快捷键() {
document.addEventListener('keydown', event => {
if (event.key === 'Enter' || event.key === '/') {
this.element.style.display = 'block'
this.element.querySelector('input').focus()
}
})
}
async 从本地载入消息() {
const data = await values(this.store)
data.map(item => ({ timestamp: new Date(item.time).getTime(), ...item }))
.filter(item => !item.ban)
.sort((a, b) => a.timestamp - b.timestamp).forEach(item => {
this.添加元素(item)
})
}
async 筛选指定范围的消息({ start, end }) {
const data = await values(this.store)
return data.map(item => ({ timestamp: new Date(item.time).getTime(), ...item })).filter(item => {
const timestamp = new Date(item.time).getTime()
return timestamp >= start && timestamp <= end && !item.ban
})
}
// 检查本地不存在的id,存储消息
async 合并消息列表(list) {
const data = await values(this.store)
const ids = data.map(item => item.id)
list.filter(item => !ids.includes(item.id)).forEach(item => {
this.添加元素(item)
this.存储消息(item)
})
}
// 收到应答(对方确认消息已被接收)
answer(data) {
const { name, text, time, type } = data
const item = this.add({ name, text, time, type })
item.classList.add('disable')
}
// 添加一条消息
add({ name, text, time, type }) {
// 如果和上一条消息是同一人, 且时间间隔小于1小时, 则向上合并
// && new Date(time).getTime() - new Date(this.last.time).getTime() < 1000 * 60 * 60
console.log('添加一条消息', this.last, name, text, time, type)
if (this.last && this.last.name === name) {
this.last.item.appendChild(Span({ textContent: text }))
this.last = { name, text, time, type, item: this.last.item }
return this.last.item
}
const item = ListItem({
classList: [type],
children: [
Span({ textContent: `${name} ${time} ${text}` })
]
})
this.ul.appendChild(item)
this.ul.scrollTop = this.ul.scrollHeight
// 记录到上一条消息
this.last = { name, text, time, type, item }
return item
}
send(text) {
if (this.event.onsend) {
this.event.onsend(text)
}
}
添加元素(data) {
// 人类可读的时间: 今天,昨天, 空字符串
function convertTimestampToReadableTime(timestamp) {
const date = new Date(timestamp);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return '今天';
} else if (date.toDateString() === yesterday.toDateString()) {
return '昨天';
} else {
return '';
}
}
// 如果上一条日期不等于这一条日期,则添加日期(只计算日期,不计算时间)
if (!this.last || this.last.time.split(' ')[0] !== data.time.split(' ')[0]) {
this.ul.appendChild(ListItem({
style: { listStyle: 'none', maxWidth: '24rem', textAlign: 'center' },
children: [
Span({
style: {
display: 'inline-block',
padding: '.1rem .5rem',
marginTop: '2rem',
boxSizing: 'border-box',
boxShadow: '0 0 1rem #eee',
borderRadius: '1rem',
fontSize: '12px',
},
textContent: `${data.time.split(' ')[0]} ${convertTimestampToReadableTime(new Date(data.time).getTime())}`
})
]
}))
}
// 如果和上一条消息是同一人, 且时间间隔小于1小时, 则向上合并
console.log('添加一条消息', this.last, data)
if (this.last && this.last.name === data.name && new Date(data.time).getTime() - new Date(this.last.time).getTime() < 1000 * 60 * 60) {
this.last.item.querySelector('ul').appendChild(ListItem({ textContent: data.text }))
this.last = { ...data, item: this.last.item }
return this.last.item
}
// 将时间转换为人类可读的格式: 如果是今天,则显示时间,如果是昨天,则显示昨天,如果是今年,则显示月日,如果是去年,则显示年月日
const redate = (str) => {
const date = new Date(str)
const now = new Date()
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const nowYear = now.getFullYear()
const nowMonth = now.getMonth() + 1
const nowDay = now.getDate()
if (year === nowYear && month === nowMonth && day === nowDay) {
return `${hour}:${minute}`
}
if (year === nowYear && month === nowMonth && day === nowDay - 1) {
return `昨天 ${hour}:${minute}`
}
if (year === nowYear) {
return `${month}${day}${hour}:${minute}`
}
return `${year}${month}${day}${hour}:${minute}`
}
const scroll = this.ul.scrollHeight === this.ul.clientHeight + this.ul.scrollTop
this.ul.appendChild(ListItem({
style: {
margin: '1rem',
padding: '.5rem 1rem',
boxSizing: 'border-box',
boxShadow: '0 0 1rem #eee',
maxWidth: '24rem',
borderRadius: '1rem',
listStyle: 'none',
},
children: [
createElement({
style: {
display: 'flex',
alignItems: 'end',
justifyContent: 'space-between',
gap: '.25rem',
},
children: [
Span({ textContent: `${data.name}` }),
Span({
style: {
color: '#888',
fontSize: '12px',
},
//textContent: `${redate(data.time)}`
// 只取时间, 不要秒
textContent: `${data.time.split(' ')[1].split(':')[0]}:${data.time.split(' ')[1].split(':')[1]}`
}),
Button({
style: {
boxSizing: 'border-box',
boxShadow: '0 0 1rem #eee',
borderRadius: '1rem',
fontSize: '12px',
color: '#555',
marginLeft: 'auto',
whiteSpace: 'nowrap',
},
textContent: '移除',
title: '加入屏蔽列表不再被渲染',
onclick: event => {
event.target.parentNode.remove()
update(data.id, item => ({ ban: true, ...item }), this.store)
}
})
]
}),
List({
style: {
listStyle: 'disc',
},
children: [
ListItem({ textContent: `${data.text}` }),
]
}),
]
}))
if (scroll) {
console.log('滚动到底部')
this.ul.scrollTop = this.ul.scrollHeight
}
// 记录到上一条消息
this.last = { ...data, item: this.ul.lastChild }
}
async 存储消息(data) {
// 检查id是否已经存在
const item = await get(data.id, this.store)
if (item) return;
await set(data.id, data, this.store)
}
发送消息(text) {
const name = localStorage.getItem('username')
const id = window.crypto.randomUUID()
const time = new Date().toLocaleString()
const type = 'text'
const data = { id, name, text, time, type }
this.添加元素(data)
this.存储消息(data)
this.send(data)
}
收到消息(data) {
console.log('收到消息', data)
this.添加元素(data)
this.存储消息(data)
}
}