Compare commits

..

5 Commits

Author SHA1 Message Date
Derek bcd09d2521 Add basic suport for media in messages 2022-02-15 18:32:04 -07:00
Derek 285c86ed39 Dont attempt to bypass the websocket connect
Caused duplications if it went multiple loops without connecting and then connected
2022-02-15 18:31:20 -07:00
Derek b2e32aa105 Set text size everywhere 2022-02-01 23:34:15 -07:00
Derek 81d909d5db Improve websocket connection 2022-02-01 23:34:05 -07:00
Derek 370b845036 Images in da chat!!!!!!!!!!!!! 2022-02-01 23:33:44 -07:00
10 changed files with 161 additions and 28 deletions

58
package-lock.json generated
View File

@ -14,6 +14,7 @@
"polished": "^4.1.3", "polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"styled-components": "^5.3.3" "styled-components": "^5.3.3"
} }
@ -7932,6 +7933,14 @@
"he": "bin/he" "he": "bin/he"
} }
}, },
"node_modules/history": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"dependencies": {
"@babel/runtime": "^7.7.6"
}
},
"node_modules/hoist-non-react-statics": { "node_modules/hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -13279,6 +13288,30 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-router": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"dependencies": {
"history": "^5.2.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"dependencies": {
"history": "^5.2.0",
"react-router": "6.2.1"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-scripts": { "node_modules/react-scripts": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@ -21878,6 +21911,14 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
}, },
"history": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"requires": {
"@babel/runtime": "^7.7.6"
}
},
"hoist-non-react-statics": { "hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -25600,6 +25641,23 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
}, },
"react-router": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"requires": {
"history": "^5.2.0"
}
},
"react-router-dom": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"requires": {
"history": "^5.2.0",
"react-router": "6.2.1"
}
},
"react-scripts": { "react-scripts": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",

View File

@ -9,6 +9,7 @@
"polished": "^4.1.3", "polished": "^4.1.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"styled-components": "^5.3.3" "styled-components": "^5.3.3"
}, },

View File

@ -1,14 +1,34 @@
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState, useRef } from 'react';
import { useQuery } from '../../hooks';
import ChatLog from '../chat-log'; import ChatLog from '../chat-log';
const maxsize = 25; const maxsize = 25;
const retryLoop = async (func) => {
try {
return await func();
} catch (error) {
retryLoop(func);
}
};
function App() { function App() {
const [messages, setMessages] = useState(Array(maxsize).fill(null)); const [messages, setMessages] = useState(Array(maxsize).fill(null));
const query = useQuery();
const socket = useRef(null); const socket = useRef(null);
useEffect(() => { useEffect(() => {
const websocketAddress = query.get('ws') ?? 'ws://127.0.0.1:8080';
const attemptConnect = async () => {
const ws = new WebSocket(websocketAddress);
ws.addEventListener('message', onEvent);
ws.addEventListener('error', onDiscconect);
ws.addEventListener('close', onDiscconect);
socket.current = ws
};
const onEvent = function (event) { const onEvent = function (event) {
setMessages((state) => { setMessages((state) => {
const newState = [...state]; const newState = [...state];
@ -17,20 +37,19 @@ function App() {
return newState; return newState;
}); });
}; };
const onDiscconect = function (event) {
retryLoop(attemptConnect);
}
const attemptConnect = () => { onDiscconect();
socket.current = new WebSocket('ws://127.0.0.1:13337');
const onError = () => setTimeout(attemptConnect, 1000);
socket.current.addEventListener('error', onError);
socket.current.addEventListener('message', onEvent);
};
attemptConnect();
return () => { return () => {
socket.current.removeEventListener('message', onEvent); socket.current.removeEventListener('message', onEvent);
socket.current.removeEventListener('error', onDiscconect);
socket.current.removeEventListener('close', onDiscconect);
socket.current.close(); socket.current.close();
} }
}, [socket, maxsize, setMessages]); }, [socket, maxsize, setMessages, query]);
return ( return (
<ChatLog log={messages} /> <ChatLog log={messages} />

View File

@ -9,6 +9,8 @@ const elementMap = {
null: Spacer, null: Spacer,
}; };
const selfChats = ['self', 'birbspace'];
function ChatLog({ function ChatLog({
log, log,
}) { }) {
@ -19,8 +21,9 @@ function ChatLog({
} }
const bestRenderableInterface = event.type.find((interfaceName) => Object.keys(elementMap).includes(interfaceName)); const bestRenderableInterface = event.type.find((interfaceName) => Object.keys(elementMap).includes(interfaceName));
const renderer = elementMap[bestRenderableInterface]; const renderer = elementMap[bestRenderableInterface];
return [event, renderer]; const deemphasize = !selfChats.includes(event.data.via ?? 'unknown');
}).filter(([event, renderer]) => !!renderer); return [event, renderer, deemphasize];
}).filter(([event, renderer, deemphasize]) => !!renderer);
useEffect(() => { useEffect(() => {
listRef.current.scrollTop = -1; listRef.current.scrollTop = -1;
@ -28,8 +31,8 @@ function ChatLog({
return ( return (
<List ref={listRef}> <List ref={listRef}>
{renderableEvents.map(([event, Element]) => ( {renderableEvents.map(([event, Element, deemphasize]) => (
<Element key={event.data.id} {...event.data} /> <Element key={event.data.id} deemphasize={deemphasize} {...event.data} />
))} ))}
</List> </List>
); );

View File

@ -19,6 +19,9 @@ export const List = styled.div`
width: 0px; width: 0px;
background: transparent; background: transparent;
} }
font-family: 'Source Code Pro', monospace;
font-size: 24px;
`; `;
export const Spacer = styled.div` export const Spacer = styled.div`

View File

@ -1,8 +1,9 @@
import { useMemo } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { XTERM_COLORS } from '../../../index.style'; import { XTERM_COLORS } from '../../../index.style';
import { Wrapper, Author, Body, Emote } from './message.style'; import { Wrapper, Author, Body, Emote, Attachments } from './message.style';
const stringHashcode = (str) => { const stringHashcode = (str) => {
var hash = 0, i, chr; var hash = 0, i, chr;
@ -17,27 +18,49 @@ const stringHashcode = (str) => {
const emoteRegex = /:([^:\s]+):/g; const emoteRegex = /:([^:\s]+):/g;
const elForType = {
image: (src) => <img src={src} />,
audio: (src) => <audio src={src} />,
video: (src) => <video src={src} />,
};
function Message({ function Message({
user_id, user_name, user_type, user_color, text, emotes, user_id, user_name, user_type, user_color, text, emotes, attachments,
deemphasize, deemphasize,
}) { }) {
const fallbackColor = XTERM_COLORS[(stringHashcode(user_id.toString()) % 144) + 88]; const fallbackColor = XTERM_COLORS[(stringHashcode(user_id.toString()) % 144) + 88];
const displayFiles = useMemo(() => {
if (attachments) {
const filtered = attachments
.map(([type, url]) => [type.split('/')[0], url])
.filter(([type, url]) => Object.keys(elForType).includes(type));
return filtered;
}
return null;
}, [attachments]);
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>
<Body> {!!text && (
{text.split(emoteRegex).map((textOrEmote, index) => { <Body>
if (textOrEmote === "") return null; {text.split(emoteRegex).map((textOrEmote, index) => {
if (index % 2 === 0) { if (textOrEmote === "") return null;
return <span>{textOrEmote}</span>; if (index % 2 === 0) {
} else { return <span>{textOrEmote}</span>;
return <Emote src={emotes?.[textOrEmote]} />; } else {
} return <Emote src={emotes?.[textOrEmote]} title={textOrEmote} />;
}).filter((item) => item !== null)} }
</Body> }).filter((item) => item !== null)}
</Body>
)}
{!!displayFiles && (
<Attachments>
{displayFiles.map(([type, url]) => elForType[type](url))}
</Attachments>
)}
</Wrapper> </Wrapper>
); );
} }
@ -47,17 +70,20 @@ 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,
text: PropTypes.node.isRequired, text: PropTypes.node,
emotes: PropTypes.shape({ emotes: PropTypes.shape({
[PropTypes.string]: PropTypes.string, [PropTypes.string]: PropTypes.string,
}), }),
deemphasize: PropTypes.bool, deemphasize: PropTypes.bool,
attachments: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
}; };
Message.defaultProps = { Message.defaultProps = {
text: '',
user_color: undefined, user_color: undefined,
emotes: {}, emotes: {},
deemphasize: false, deemphasize: false,
attachments: null,
}; };
export default Message; export default Message;

View File

@ -65,8 +65,6 @@ const setColors = ({ type, color, deemphasize }) => {
export const Wrapper = styled.div` export const Wrapper = styled.div`
${setColors} ${setColors}
font-family: 'Source Code Pro', monospace;
font-size: 18px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
@ -93,3 +91,19 @@ export const Wrapper = styled.div`
animation: 160s fade cubic-bezier(.6,.04,.98,.34), 0.3s slide cubic-bezier(.08,.82,.17,1); 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;
`; `;
export const Attachments = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px 6px;
padding: 8px;
img {
flex: 1;
min-width: 33%;
max-width: 100%;
max-height: 300px;
object-fit: scale-down;
}
`;

1
src/hooks/index.js Normal file
View File

@ -0,0 +1 @@
export { default as useQuery } from './useQuery';

5
src/hooks/useQuery.js Normal file
View File

@ -0,0 +1,5 @@
import { useLocation } from 'react-router-dom';
export default function useQuery() {
return new URLSearchParams(useLocation().search);
}

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import App from './components/app/app'; import App from './components/app/app';
import { GlobalStyle } from './index.style'; import { GlobalStyle } from './index.style';
@ -7,7 +8,9 @@ import { GlobalStyle } from './index.style';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<GlobalStyle /> <GlobalStyle />
<App /> <BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );