diff --git a/chat/global.d.ts b/chat/global.d.ts index e448caa9d..978e81e9a 100644 --- a/chat/global.d.ts +++ b/chat/global.d.ts @@ -2,7 +2,8 @@ declare var process: { env: Record; }; -declare interface ChatSpeakResponse { +declare interface IChatMessage { + id: string; username: string; avatar: string; hat: string; @@ -11,6 +12,7 @@ declare interface ChatSpeakResponse { text_censored: string; text_html: string; time: number; + quotes: null | string; } declare interface EmojiModSelection { diff --git a/chat/package.json b/chat/package.json index 48fa4a477..b3244f714 100644 --- a/chat/package.json +++ b/chat/package.json @@ -1,6 +1,6 @@ { "name": "chat", - "version": "0.0.9", + "version": "0.0.10", "main": "index.js", "license": "MIT", "dependencies": { diff --git a/chat/src/features/activity/Activity.css b/chat/src/features/activity/Activity.css deleted file mode 100644 index 7868f62f6..000000000 --- a/chat/src/features/activity/Activity.css +++ /dev/null @@ -1,21 +0,0 @@ -.Activity { - display: flex; - align-items: center; - padding: 1rem; - margin: 0 4rem; -} - -.Activity > section { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 1rem; -} - -.Activity i { - margin-right: 3rem; -} - -.Activity > section:not(:last-child) { - margin-right: 2rem; -} \ No newline at end of file diff --git a/chat/src/features/activity/Activity.tsx b/chat/src/features/activity/Activity.tsx deleted file mode 100644 index 7d4e56dd8..000000000 --- a/chat/src/features/activity/Activity.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React from "react"; -import "./Activity.css"; - -const ACTIVITIES = [ - { - icon: "circle", - title: "Roulette", - description: "Round and round the wheel of fate turns.", - }, - { - icon: "cards", - title: "Blackjack", - description: "Twenty one ways to change your life.", - }, - { - icon: "dollar-sign", - title: "Slots", - description: "Today's your lucky day.", - }, - { - icon: "dollar-sign", - title: "Racing", - description: "Make it all back at the track.", - }, - { icon: "dollar-sign", title: "Crossing", description: "Take a load off." }, -]; - -export function Activity() { - return ( -
- {ACTIVITIES.map((activity) => ( -
- -

{activity.title}

-
- ))} -
- ); -} diff --git a/chat/src/features/activity/index.ts b/chat/src/features/activity/index.ts deleted file mode 100644 index d9f56adb2..000000000 --- a/chat/src/features/activity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Activity"; diff --git a/chat/src/features/chat/Chat.css b/chat/src/features/chat/Chat.css deleted file mode 100644 index 1ff272bba..000000000 --- a/chat/src/features/chat/Chat.css +++ /dev/null @@ -1,56 +0,0 @@ -.Chat { - display: flex; - align-items: stretch; - justify-content: center; - position: relative; - overflow: hidden; - width: 100%; - min-width: 350px; - max-width: 1200px; -} - -.Chat-mobile-top { - display: none; - align-items: center; - justify-content: flex-end; - margin-bottom: 1rem; -} - -.Chat-mobile-top i { - margin-right: 0.25rem; - position: relative; - top: -1px; -} - -.Chat-mobile-top span { - font-size: 18px; -} - -.Chat-side { - display: flex; - flex-direction: column; - width: 40%; -} - -@media screen and (max-width: 1180px) { - .Chat-mobile-top { - display: flex; - } - .Chat-side { - display: none; - } -} - -.Chat-drawer { - padding-right: 1rem; -} - -.Chat-typing { - height: 18px; - display: inline-block; -} - - -.Chat-window { - width: 100%; -} \ No newline at end of file diff --git a/chat/src/features/chat/Chat.tsx b/chat/src/features/chat/Chat.tsx deleted file mode 100644 index ed3b106f5..000000000 --- a/chat/src/features/chat/Chat.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from "react"; -import { useChat, useDrawer } from "../../hooks"; -import { ActivityList } from "./ActivityList"; -import { ChatMessageList } from "./ChatMessage"; -import { QuotedMessage } from "./QuotedMessage"; -import { UserInput } from "./UserInput"; -import { UserList } from "./UserList"; -import "./Chat.css"; - -export function Chat() { - const { reveal } = useDrawer(); - const { online, quote } = useChat(); - - return ( -
-
-
- -
- - {quote && } - -
-
- - {process.env.FEATURES_ACTIVITY && } -
-
- ); -} \ No newline at end of file diff --git a/chat/src/features/chat/ChatMessage.tsx b/chat/src/features/chat/ChatMessage.tsx index d930a6a1f..d3d532faa 100644 --- a/chat/src/features/chat/ChatMessage.tsx +++ b/chat/src/features/chat/ChatMessage.tsx @@ -6,13 +6,17 @@ import { Username } from "./Username"; import { useChat, useRootContext } from "../../hooks"; import "./ChatMessage.css"; -interface ChatMessageProps extends ChatSpeakResponse { +interface ChatMessageProps extends IChatMessage { showUser?: boolean; } const TIMESTAMP_UPDATE_INTERVAL = 20000; +const SCROLL_TO_QUOTED_OVERFLOW = 250; +const QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION = 2500; +const QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH = 30; export function ChatMessage({ + id, avatar, showUser = true, namecolor, @@ -22,8 +26,10 @@ export function ChatMessage({ text_html, text_censored, time, + quotes, }: ChatMessageProps) { const message = { + id, avatar, namecolor, username, @@ -32,6 +38,7 @@ export function ChatMessage({ text_html, text_censored, time, + quotes, }; const { username: loggedInUsername, @@ -39,17 +46,12 @@ export function ChatMessage({ censored, themeColor, } = useRootContext(); - const { quote, deleteMessage, quoteMessage } = useChat(); + const { quote, messageLookup, deleteMessage, quoteMessage } = useChat(); const content = censored ? text_censored : text_html; const hasMention = content.includes(loggedInUsername); const mentionStyle = hasMention ? { backgroundColor: `#${themeColor}55` } : {}; - const quoteStyle = - quote?.username === username && quote?.text === text && quote?.time === time - ? { borderLeft: `2px solid #${themeColor}` } - : {}; - const style = { ...mentionStyle, ...quoteStyle }; const [confirmedDelete, setConfirmedDelete] = useState(false); const handleDeleteMessage = useCallback(() => { if (confirmedDelete) { @@ -72,7 +74,7 @@ export function ChatMessage({ }, []); return ( -
+
{showUser && (
{timestamp}
)} + {quotes && ( + { + const element = document.getElementById(quotes); + + if (element) { + element.scrollIntoView(); + element.style.background = `#${themeColor}33`; + + setTimeout(() => { + element.style.background = "unset"; + }, QUOTED_MESSAGE_CONTEXTUAL_HIGHLIGHTING_DURATION); + + const [appContent] = Array.from( + document.getElementsByClassName("App-content") + ); + + if (appContent) { + appContent.scrollTop -= SCROLL_TO_QUOTED_OVERFLOW; + } + } + }} + > + Replying to @{messageLookup[quotes]?.username}:{" "} + + " + {messageLookup[quotes]?.text.slice( + 0, + QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH + )} + {messageLookup[quotes]?.text.length >= + QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH + ? "..." + : ""} + " + + + )}
(null); const { visible, addQuery } = useEmojis(); @@ -29,7 +29,9 @@ export function UserInput() { const emojiSegment = input.slice(openEmojiToken + 1, closeEmojiToken + 1); updateDraft(input); - addQuery(openEmojiToken === -1 ? "" : emojiSegment); + addQuery( + openEmojiToken === -1 || emojiSegment.includes(" ") ? "" : emojiSegment + ); setTypingOffset( emojiSegment.length * process.env.APPROXIMATE_CHARACTER_WIDTH ); @@ -68,7 +70,6 @@ export function UserInput() { builtChatInput.current?.focus(); hide(); }, [hide]); - const handleSelectEmoji = useCallback((emoji: string) => { updateDraft((prev) => `${prev} :${emoji}: `); }, []); @@ -128,7 +129,7 @@ export function UserInput() { role="button" onClick={handleCloseEmojiDrawer} className="UserInput-emoji" - style={{top: 6}} + style={{ top: 6 }} > X diff --git a/chat/src/features/chat/index.ts b/chat/src/features/chat/index.ts index e6696eef0..8c085a6c8 100644 --- a/chat/src/features/chat/index.ts +++ b/chat/src/features/chat/index.ts @@ -1,5 +1,3 @@ -export * from "./ActivityList"; -export * from "./Chat"; export * from "./ChatHeading"; export * from "./ChatMessage"; export * from "./QuotedMessage"; diff --git a/chat/src/features/index.ts b/chat/src/features/index.ts index 05e9e348b..74a333fea 100644 --- a/chat/src/features/index.ts +++ b/chat/src/features/index.ts @@ -1,3 +1,2 @@ -export * from "./activity"; export * from "./chat"; export * from "./emoji"; diff --git a/chat/src/hooks/useChat.tsx b/chat/src/hooks/useChat.tsx index 1898d24ca..5e89ad5a6 100644 --- a/chat/src/hooks/useChat.tsx +++ b/chat/src/hooks/useChat.tsx @@ -10,6 +10,7 @@ import React, { } from "react"; import { io, Socket } from "socket.io-client"; import lozad from "lozad"; +import debounce from "lodash.debounce"; import { useRootContext } from "./useRootContext"; import { useWindowFocus } from "./useWindowFocus"; @@ -25,12 +26,13 @@ enum ChatHandlers { interface ChatProviderContext { online: string[]; typing: string[]; - messages: ChatSpeakResponse[]; + messages: IChatMessage[]; draft: string; - quote: null | ChatSpeakResponse; + quote: null | IChatMessage; + messageLookup: Record; updateDraft: React.Dispatch>; sendMessage(): void; - quoteMessage(message: null | ChatSpeakResponse): void; + quoteMessage(message: null | IChatMessage): void; deleteMessage(withText: string): void; } @@ -40,38 +42,43 @@ const ChatContext = createContext({ messages: [], draft: "", quote: null, + messageLookup: {}, updateDraft() {}, sendMessage() {}, quoteMessage() {}, deleteMessage() {}, }); +const MINIMUM_TYPING_UPDATE_INTERVAL = 250; + export function ChatProvider({ children }: PropsWithChildren) { const { username, siteName } = useRootContext(); const socket = useRef(null); const [online, setOnline] = useState([]); const [typing, setTyping] = useState([]); - const [messages, setMessages] = useState([]); + const [messages, setMessages] = useState([]); const [draft, setDraft] = useState(""); - const [quote, setQuote] = useState(null); + const lastDraft = useRef(""); + const [quote, setQuote] = useState(null); const focused = useWindowFocus(); const [notifications, setNotifications] = useState(0); - const addMessage = useCallback((message: ChatSpeakResponse) => { + const [messageLookup, setMessageLookup] = useState({}); + const addMessage = useCallback((message: IChatMessage) => { setMessages((prev) => prev.concat(message)); - + if (message.username !== username && !document.hasFocus()) { setNotifications((prev) => prev + 1); } }, []); const sendMessage = useCallback(() => { - const message = quote - ? `> ${quote.text}\n@${quote.username}

${draft}` - : draft; - socket.current?.emit(ChatHandlers.SPEAK, message); + socket.current?.emit(ChatHandlers.SPEAK, { + message: draft, + quotes: quote?.id ?? null, + }); setQuote(null); setDraft(""); - }, [draft]); + }, [draft, quote]); const requestDeleteMessage = useCallback((withText: string) => { socket.current?.emit(ChatHandlers.DELETE, withText); }, []); @@ -84,7 +91,7 @@ export function ChatProvider({ children }: PropsWithChildren) { setQuote(null); } }, []); - const quoteMessage = useCallback((message: ChatSpeakResponse) => { + const quoteMessage = useCallback((message: IChatMessage) => { setQuote(message); try { @@ -98,6 +105,7 @@ export function ChatProvider({ children }: PropsWithChildren) { messages, draft, quote, + messageLookup, quoteMessage, sendMessage, deleteMessage: requestDeleteMessage, @@ -109,6 +117,7 @@ export function ChatProvider({ children }: PropsWithChildren) { messages, draft, quote, + messageLookup, sendMessage, deleteMessage, quoteMessage, @@ -128,8 +137,18 @@ export function ChatProvider({ children }: PropsWithChildren) { } }); + const debouncedTypingUpdater = useMemo( + () => + debounce( + () => socket.current?.emit(ChatHandlers.TYPING, lastDraft.current), + MINIMUM_TYPING_UPDATE_INTERVAL + ), + [] + ); + useEffect(() => { - socket.current?.emit(ChatHandlers.TYPING, draft); + lastDraft.current = draft; + debouncedTypingUpdater(); }, [draft]); useEffect(() => { @@ -138,6 +157,15 @@ export function ChatProvider({ children }: PropsWithChildren) { } }, [focused]); + useEffect(() => { + setMessageLookup( + messages.reduce((prev, next) => { + prev[next.id] = next; + return prev; + }, {} as Record) + ); + }, [messages]); + // Display e.g. [+2] Chat when notifications occur when you're away. useEffect(() => { const title = document.getElementsByTagName("title")[0]; diff --git a/files/assets/js/chat_done.css b/files/assets/js/chat_done.css deleted file mode 100644 index baa7f192a..000000000 --- a/files/assets/js/chat_done.css +++ /dev/null @@ -1 +0,0 @@ -.Activity{display:flex;align-items:center;padding:1rem;margin:0 4rem}.Activity>section{display:flex;align-items:center;justify-content:space-between;padding:0 1rem}.Activity i{margin-right:3rem}.Activity>section:not(:last-child){margin-right:2rem}.ActivityList{margin-left:2rem}.ActivityList h4{display:flex;align-items:center;justify-content:space-between}.ActivityList h4 hr{flex:1;margin-right:1rem}.ActivityList-activity{display:flex;align-items:center;justify-content:space-between;cursor:pointer}.ActivityList-activity-icon{margin-right:1rem}.ActivityList-activity{transition:background .4s ease-in-out;padding:0 1rem}.ActivityList-activity:hover{cursor:pointer;background:#ffffff05}.Username{display:inline-flex;align-items:center}.Username>a{font-weight:700;margin-left:8px}@keyframes fading-in{0%{opacity:0}to{opacity:1}}.ChatMessage{padding:.5rem 3rem .5rem .5rem;position:relative;animation:fading-in .3s ease-in-out forwards}.ChatMessage-top{display:flex;align-items:center}.ChatMessage-timestamp{margin-left:.5rem}.ChatMessage-bottom{display:flex;align-items:center;justify-content:space-between;padding-left:30px}.ChatMessage-content{margin-right:.5rem;word-break:break-all;display:inline-block}.ChatMessage-button{background:transparent!important}.ChatMessage-button__confirmed i{color:red!important}.ChatMessage-delete{position:absolute;top:4px;right:4px}.ChatMessageList{flex:1}.QuotedMessage{display:flex;align-items:center;justify-content:space-between}.QuotedMessage-content{margin-left:1rem;flex:1;max-width:420px;max-height:40px;overflow:hidden;text-overflow:ellipsis;margin-right:1rem}.BaseDrawer{flex:1;padding-right:2rem;overflow:hidden}.EmojiDrawer-options{display:flex;align-items:center;justify-content:space-between}.UserInput{position:relative}.UserInput-emoji{cursor:pointer;position:absolute;top:12px;right:12px;font-size:20px}.UserList{padding:1rem;border-left:1px dotted var(--primary);flex:1;position:absolute;top:0;left:0;width:100%;height:100%;overflow:auto}.UserList-heading{display:flex;align-items:center;justify-content:space-between}.UserList-heading h5{margin-right:2rem}.UserList ul::-webkit-scrollbar{display:none}.Chat{display:flex;align-items:stretch;justify-content:center;position:relative;overflow:hidden;width:100%;min-width:350px;max-width:1200px}.Chat-mobile-top{display:none;align-items:center;justify-content:flex-end;margin-bottom:1rem}.Chat-mobile-top i{margin-right:.25rem;position:relative;top:-1px}.Chat-mobile-top span{font-size:18px}.Chat-side{display:flex;flex-direction:column;width:40%}@media screen and (max-width: 1180px){.Chat-mobile-top{display:flex}.Chat-side{display:none}}.Chat-drawer{padding-right:1rem}.Chat-typing{height:18px;display:inline-block}.Chat-window{width:100%}.ChatHeading{flex:1;display:flex;align-items:center;justify-content:space-between}.ChatHeading i{margin-right:.5rem}.UsersTyping{height:18px;display:inline-block}html,body{overscroll-behavior-y:none}.App{position:fixed;width:100vw;display:flex;overflow:hidden}.App-wrapper{flex:1;overflow:hidden;display:flex;flex-direction:column;margin:0 auto;max-width:1000px}.App-heading{flex-basis:3rem;border-bottom:1px dashed var(--primary);display:flex;align-items:center}.App-heading small{opacity:.2}.App-side{height:100%;flex:1;background:var(--gray-500);position:relative}.App-content{position:relative;flex:3;height:60vh;max-height:1000px;overflow:auto;-ms-overflow-style:none;scrollbar-width:none;display:flex;flex-direction:column;border-bottom:1px dashed var(--primary)}.App-content::-webkit-scrollbar{display:none}.App-drawer{z-index:2;display:flex;background:var(--background);height:100%}.App-center,.App-bottom-wrapper{display:flex;align-items:flex-start}.App-bottom{flex:3}.App-bottom-dummy{flex:1}.App-bottom-extra{padding:1rem;height:64px}@media screen and (max-width: 1100px){.App-side,.App-bottom-dummy{display:none}.App-bottom-wrapper{padding-right:1rem;padding-left:1rem}}lite-youtube{min-width:min(80vw,500px)} diff --git a/files/routes/chat.py b/files/routes/chat.py index e9bfa0f14..e9cc4e787 100644 --- a/files/routes/chat.py +++ b/files/routes/chat.py @@ -1,4 +1,5 @@ import time +import uuid from files.helpers.jinja2 import timestamp from files.helpers.wrappers import * from files.helpers.sanitize import sanitize @@ -54,11 +55,17 @@ def speak(data, v): global messages, total if SITE == 'rdrama.net': text = data[:200].strip() - else: text = data[:1000].strip() + else: text = data['message'][:1000].strip() if not text: return '', 403 text_html = sanitize(text, count_marseys=True) + print("\n\n\n\n\n\n\n\n\n") + print("\n\n\n\n\n\n\n\n\n") + print(data) + quotes = data['quotes'] data={ + "id": str(uuid.uuid4()), + "quotes": quotes, "avatar": v.profile_url, "hat": v.hat_active, "username": v.username,