Compare commits
3 Commits
85870d1f7d
...
22ef8bb29c
Author | SHA1 | Date |
---|---|---|
Derek | 22ef8bb29c | |
Derek | 14ec6550fa | |
Derek | 0083793ba3 |
|
@ -2,19 +2,19 @@ import { useEffect, useState, useRef } from 'react';
|
||||||
|
|
||||||
import ChatLog from '../chat-log';
|
import ChatLog from '../chat-log';
|
||||||
|
|
||||||
const maxsize = 100;
|
const maxsize = 25;
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState(Array(maxsize).fill(null));
|
||||||
const socket = useRef(null);
|
const socket = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onEvent = function (event) {
|
const onEvent = function (event) {
|
||||||
setMessages((state) => {
|
setMessages((state) => {
|
||||||
if (state.length >= maxsize) {
|
const newState = [...state];
|
||||||
state.pop();
|
newState.unshift(JSON.parse(event.data));
|
||||||
}
|
newState.pop();
|
||||||
return [JSON.parse(event.data), ...state];
|
return newState;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,36 @@
|
||||||
|
import { useRef, useEffect } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Message from './message';
|
import Message from './message';
|
||||||
|
|
||||||
import { List } from './chat-log.style';
|
import { List, Spacer } from './chat-log.style';
|
||||||
|
|
||||||
const elementMap = {
|
const elementMap = {
|
||||||
Message,
|
Message,
|
||||||
|
null: Spacer,
|
||||||
};
|
};
|
||||||
|
|
||||||
function ChatLog({
|
function ChatLog({
|
||||||
log,
|
log,
|
||||||
}) {
|
}) {
|
||||||
// TODO: Should look at full MRO
|
const listRef = useRef();
|
||||||
const renderableEvents = log.filter((event) => Object.keys(elementMap).includes(event.type[0]))
|
const renderableEvents = log.map((event) => {
|
||||||
|
if (!event?.type) {
|
||||||
|
event = { type: ['null'], data: { }};
|
||||||
|
}
|
||||||
|
const bestRenderableInterface = event.type.find((interfaceName) => Object.keys(elementMap).includes(interfaceName));
|
||||||
|
const renderer = elementMap[bestRenderableInterface];
|
||||||
|
return [event, renderer];
|
||||||
|
}).filter(([event, renderer]) => !!renderer);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
listRef.current.scrollTop = -1;
|
||||||
|
}, [renderableEvents]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<List>
|
<List ref={listRef}>
|
||||||
{renderableEvents.map((event) => {
|
{renderableEvents.map(([event, Element]) => (
|
||||||
const Element = elementMap[event.type[0]];
|
<Element key={event.data.id} {...event.data} />
|
||||||
return (
|
))}
|
||||||
<Element key={event.data.id} {...event.data} />
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import styled from 'styled-components';
|
||||||
|
|
||||||
export const List = styled.div`
|
export const List = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
|
@ -9,4 +10,17 @@ export const List = styled.div`
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|
||||||
padding: 12px 8px;
|
padding: 12px 8px;
|
||||||
|
|
||||||
|
overflow: scroll;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
|
scrollbar-width: none;
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0px;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Spacer = styled.div`
|
||||||
|
min-height: 2em;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import { XTERM_COLORS } from '../../../index.style';
|
import { XTERM_COLORS } from '../../../index.style';
|
||||||
|
|
||||||
import { Wrapper, Author } from './message.style';
|
import { Wrapper, Author, Body, Emote } from './message.style';
|
||||||
|
|
||||||
const stringHashcode = (str) => {
|
const stringHashcode = (str) => {
|
||||||
var hash = 0, i, chr;
|
var hash = 0, i, chr;
|
||||||
|
@ -15,17 +15,29 @@ const stringHashcode = (str) => {
|
||||||
return hash;
|
return hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emoteRegex = /:([^:\s]+):/g;
|
||||||
|
|
||||||
function Message({
|
function Message({
|
||||||
user_id, user_name, user_type, user_color, text,
|
user_id, user_name, user_type, user_color, text, emotes,
|
||||||
deemphasize,
|
deemphasize,
|
||||||
}) {
|
}) {
|
||||||
const fallbackColor = XTERM_COLORS[(stringHashcode(user_id.toString()) % 144) + 88];
|
const fallbackColor = XTERM_COLORS[(stringHashcode(user_id.toString()) % 144) + 88];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper type={user_type} color={user_color ?? fallbackColor} deemphasize={deemphasize}>
|
<Wrapper type={user_type} color={user_color ?? fallbackColor} deemphasize={deemphasize}>
|
||||||
<Author>
|
<Author>
|
||||||
[ {user_name} ]
|
[ {user_name} ]
|
||||||
</Author>
|
</Author>
|
||||||
{text}
|
<Body>
|
||||||
|
{text.split(emoteRegex).map((textOrEmote, index) => {
|
||||||
|
if (textOrEmote === "") return null;
|
||||||
|
if (index % 2 === 0) {
|
||||||
|
return <span>{textOrEmote}</span>;
|
||||||
|
} else {
|
||||||
|
return <Emote src={emotes[textOrEmote]} />;
|
||||||
|
}
|
||||||
|
}).filter((item) => item !== null)}
|
||||||
|
</Body>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -35,12 +47,16 @@ Message.propTypes = {
|
||||||
user_name: PropTypes.node.isRequired,
|
user_name: PropTypes.node.isRequired,
|
||||||
user_type: PropTypes.string.isRequired,
|
user_type: PropTypes.string.isRequired,
|
||||||
user_color: PropTypes.string,
|
user_color: PropTypes.string,
|
||||||
deemphasize: PropTypes.bool,
|
|
||||||
text: PropTypes.node.isRequired,
|
text: PropTypes.node.isRequired,
|
||||||
|
emotes: PropTypes.shape({
|
||||||
|
[PropTypes.string]: PropTypes.string,
|
||||||
|
}),
|
||||||
|
deemphasize: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Message.defaultProps = {
|
Message.defaultProps = {
|
||||||
user_color: undefined,
|
user_color: undefined,
|
||||||
|
emotes: {},
|
||||||
deemphasize: false,
|
deemphasize: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,16 @@ export const Author = styled.div`
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const Body = styled.span`
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const Emote = styled.img`
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
vertical-align: middle;
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const normalizeColor = (color) => {
|
export const normalizeColor = (color) => {
|
||||||
if (color === null) {
|
if (color === null) {
|
||||||
return 'inherit';
|
return 'inherit';
|
||||||
|
@ -63,18 +73,24 @@ export const Wrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
|
|
||||||
@keyframes dissapear {
|
@keyframes fade {
|
||||||
0% {
|
0% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
15% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animation: 160s dissapear linear;
|
@keyframes slide {
|
||||||
|
from {
|
||||||
|
transform: translateX(-50px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: 160s fade cubic-bezier(.6,.04,.98,.34), 0.3s slide cubic-bezier(.08,.82,.17,1);
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
`;
|
`;
|
||||||
|
|
Loading…
Reference in New Issue