From 07d23d7efa99f690da50c54be14a95b68bf96e30 Mon Sep 17 00:00:00 2001 From: Tobias Berger <14962962+Toby222@users.noreply.github.com> Date: Mon, 17 Jan 2022 17:35:36 +0000 Subject: [PATCH] Typing indicator Also better nicknames and styling --- src/components/Author.tsx | 12 ++- src/components/Message.tsx | 18 ++-- src/lib | 2 +- src/pages/index.tsx | 184 ++++++++++++++++++++++++++++--------- src/styles.scss | 55 ++++++----- 5 files changed, 197 insertions(+), 74 deletions(-) diff --git a/src/components/Author.tsx b/src/components/Author.tsx index 504c456..7bbaf6c 100644 --- a/src/components/Author.tsx +++ b/src/components/Author.tsx @@ -2,12 +2,22 @@ import { FunctionComponent } from "react"; type AuthorComponentProps = { authorId: string; + authorNickname: string; }; const AuthorComponent: FunctionComponent = ({ authorId, + authorNickname, }) => { - return {authorId}; + return ( + + {authorNickname} + + ); }; export default AuthorComponent; diff --git a/src/components/Message.tsx b/src/components/Message.tsx index c522eb8..9b8f426 100644 --- a/src/components/Message.tsx +++ b/src/components/Message.tsx @@ -5,7 +5,9 @@ import AuthorComponent from "./Author"; type MessageComponentProps = { message: TextMessage; + authorNickname: string; }; + enum MONTH_NAMES { "January", "February", @@ -59,8 +61,8 @@ function timeAgo(dateParam: Date | number) { const isYesterday = yesterday.toDateString() === date.toDateString(); const isThisYear = today.getFullYear() === date.getFullYear(); - if (seconds < 5) { - return "now"; + if (seconds < 2) { + return "just now"; } else if (seconds < 60) { return `${seconds} seconds ago`; } else if (seconds < 90) { @@ -80,6 +82,7 @@ function timeAgo(dateParam: Date | number) { const MessageComponent: FunctionComponent = ({ message, + authorNickname, }) => { const date = new Date(message.date); const [timeAgoString, setTimeAgoString] = useState(timeAgo(message.date)); @@ -92,14 +95,17 @@ const MessageComponent: FunctionComponent = ({ } }; - const interval = setInterval(checkTimeAgo, 5000); + const interval = setInterval(checkTimeAgo, 1000); return () => clearInterval(interval); }, [message.date, timeAgoString]); return ( -
+ <>

- +

{message.content}
-
+ ); }; diff --git a/src/lib b/src/lib index 4c69876..cde3d62 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 4c698768def503209283af5d7a04c0d21c49092f +Subproject commit cde3d62b1c0a1c95643037ca03e95bac178118d2 diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 9aefc1a..6b35a0c 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,10 +1,14 @@ -import { useCallback, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import useWebSocket, { ReadyState, ReadyState as WebSocketReadyState, } from "react-use-websocket"; import MessageComponent from "src/components/Message"; -import type { TextMessage } from "src/lib/ServerMessage"; +import { + isCurrentlyTypingMessage, + TextMessage, + TypingMessage, +} from "src/lib/ServerMessage"; import { isIdResponseMessage, isServerMessage, @@ -12,17 +16,28 @@ import { MessageType, } from "src/lib/ServerMessage"; -export default function Index(): JSX.Element { - const [count, setCount] = useState(0); - const [messageHistory, setMessageHistory] = useState([]); - const [socketUrl, setSocketUrl] = useState("wss.tobot.tk:8085/"); - const [authorId, setAuthorId] = useState(""); +let keepAliveIntervals: NodeJS.Timeout[] = []; +let shouldResendTyping = true; +export default function Index(): JSX.Element { + const [messageHistory, setMessageHistory] = useState([]); + const [currentlyTyping, setCurrentlyTyping] = useState([]); + const [socketUrl, setSocketUrl] = useState("wss.tobot.tk:8085/"); + const [authorId, setAuthorId] = useState(""); + const messageInput = useRef(null); + const messageContainer = useRef(null); + + const getNickname = useCallback( + (id: string) => { + return id === authorId ? "You" : id; + }, + [authorId] + ); function onMessage(event: MessageEvent): void { const message = JSON.parse(event.data); if (!isServerMessage(message)) { - console.log("DEBUG: ", message); + console.debug("DEBUG: ", message); throw new Error(`Server sent unexpected message \`${event.data}}\``); } @@ -30,6 +45,9 @@ export default function Index(): JSX.Element { setAuthorId(message.authorId); } else if (isTextMessage(message)) { setMessageHistory([...messageHistory, message]); + console.log("scrollheight", messageContainer.current?.scrollHeight); + } else if (isCurrentlyTypingMessage(message)) { + setCurrentlyTyping(message.currently); } } @@ -43,22 +61,34 @@ export default function Index(): JSX.Element { date: Date.now(), }); }, 1000); + keepAliveIntervals.push(keepAliveInterval); }, onClose() { clearInterval(keepAliveInterval); + keepAliveIntervals = keepAliveIntervals.filter( + (interval) => interval !== keepAliveInterval + ); }, }); const handleClickSendMessage = useCallback(() => { - setCount(count + 1); - const message: TextMessage = { + if (!messageInput.current) { + return; + } + const messageText = messageInput.current?.value; + if (messageText === undefined || messageText.length === 0) { + return; + } + + messageInput.current.value = ""; + + websocket.sendJsonMessage({ author: authorId, type: MessageType.TEXT, date: Date.now(), - content: `Hello, world! ${count}`, - }; - websocket.sendJsonMessage(message); - }, [count, websocket, authorId]); + content: messageText, + } as TextMessage); + }, [authorId, websocket]); const trySetSocketUrl = useCallback( (url: string) => { @@ -77,36 +107,108 @@ export default function Index(): JSX.Element { [websocket] ); + function handleInput() { + if ( + websocket.readyState === ReadyState.OPEN && + (shouldResendTyping || !currentlyTyping.includes(authorId)) + ) { + websocket.sendJsonMessage({ + type: MessageType.TYPING, + date: Date.now(), + } as TypingMessage); + shouldResendTyping = false; + setTimeout(() => (shouldResendTyping = true), 1000); + } + } + console.log(handleInput); + + const typingIndicator = useMemo(() => { + if (currentlyTyping.length === 0) { + return <>Nobody is typing; + } + if (currentlyTyping.length === 1) { + if (currentlyTyping[0] === authorId) { + return ( + <> + You are typing... + + ); + } + return ( + <> + {getNickname(currentlyTyping[0]) === currentlyTyping[0] ? ( + currentlyTyping[0] + ) : ( + {getNickname(currentlyTyping[0])} + )}{" "} + is typing... + + ); + } + if (currentlyTyping.length < 4) { + const result = currentlyTyping.map((id, idx, arr) => ( + <> + {id === getNickname(id) ? ( + id + ) : ( + {getNickname(id)} + )} + {idx + 1 === arr.length ? "" : ", "} + + )); + result.push(<> are typing...); + + return <>{result}; + } + return <>Several people are typing...; + }, [authorId, currentlyTyping, getNickname]); + return ( - <> -
-
    - {messageHistory.map((message, idx) => ( -
  1. - -
  2. - ))} -
-
- Ready state: - - {ReadyState[websocket.readyState]} ({websocket.readyState}) - - +
+
+ {authorId === "" ? ( + <> + ) : ( + <> + Your ID: {authorId} + + )}{" "} + Ready state: + + {ReadyState[websocket.readyState]} ({websocket.readyState}) + + + trySetSocketUrl(e.target.value)} + /> +
+
    + {messageHistory.map((message, idx) => ( +
  1. + +
  2. + ))} +
+
+ {typingIndicator} + trySetSocketUrl(e.target.value)} + placeholder="Type here..." + id="message-input" + onInput={handleInput} + ref={messageInput} /> - -
-
- + + +
+
); } diff --git a/src/styles.scss b/src/styles.scss index 6cf0e98..887d9c8 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -6,19 +6,15 @@ text-align: center; } +* { + font-family: unset; +} + p { margin: 20px 0px; } button { - outline: none; - border: none; - border-radius: 0; - padding: 10px 35px; - - background: #845ec2; - color: white; - &:hover:not(:disabled) { filter: brightness(120%); cursor: pointer; @@ -31,39 +27,43 @@ button { body { margin: 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", - "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; } main { display: flex; flex-direction: column; - height: 100vh; + + height: calc(100vh - 20px); + width: calc(100vw - 20px); justify-content: space-between; + padding: 10px; + + .nickname { + font-style: italic; + } #messages-container { display: flex; flex-direction: column; + flex-grow: 1; + overflow-y: scroll; overflow-anchor: none; list-style: none; padding-inline-start: 0; - - > :nth-child(even) { - background-color: lightgray; - } - - > li { - width: 100%; - } + max-width: 100%; + overflow-wrap: break-word; .message { display: block; text-align: left; - width: 100%; + width: calc(100% - 2 * 5px); + padding: 5px; + white-space: pre-line; + + &:nth-child(even) { + background-color: #f2f2f2; + } > h2 { width: 100%; @@ -72,10 +72,15 @@ main { font-size: medium; justify-content: space-between; } - .message-date { - } } } #message-writing-area { + display: flex; + flex-direction: column; + justify-content: center; + + #message-input { + width: 80%; + } } }