diff --git a/chat/package.json b/chat/package.json index 372bf4e71..4449067b1 100644 --- a/chat/package.json +++ b/chat/package.json @@ -1,6 +1,6 @@ { "name": "chat", - "version": "0.1.26", + "version": "0.1.27", "main": "./src/index.tsx", "license": "MIT", "dependencies": { diff --git a/chat/src/features/chat/ChatMessage.css b/chat/src/features/chat/ChatMessage.css index db9b6b751..cd7fb423a 100644 --- a/chat/src/features/chat/ChatMessage.css +++ b/chat/src/features/chat/ChatMessage.css @@ -9,6 +9,10 @@ border-bottom: 1px dashed var(--primary); } +.ChatMessage__isOptimistic { + opacity: 0.5; +} + .ChatMessage p { margin: 0; } diff --git a/chat/src/features/chat/ChatMessage.tsx b/chat/src/features/chat/ChatMessage.tsx index 2170d9786..5a172e798 100644 --- a/chat/src/features/chat/ChatMessage.tsx +++ b/chat/src/features/chat/ChatMessage.tsx @@ -11,7 +11,12 @@ import key from "weak-key"; import humanizeDuration from "humanize-duration"; import cloneDeep from "lodash.clonedeep"; import { Username } from "./Username"; -import { useChat, useRootContext } from "../../hooks"; +import { + DIRECT_MESSAGE_ID, + OPTIMISTIC_MESSAGE_ID, + useChat, + useRootContext, +} from "../../hooks"; import { QuotedMessageLink } from "./QuotedMessageLink"; import "./ChatMessage.css"; @@ -34,6 +39,7 @@ export function ChatMessage({ }: ChatMessageProps) { const { id, + user_id, avatar, namecolor, username, @@ -68,6 +74,8 @@ export function ChatMessage({ (text_html.includes(`/id/${userId}`) && userUsername && username !== userUsername); + const isDirect = id === DIRECT_MESSAGE_ID; + const isOptimistic = id === OPTIMISTIC_MESSAGE_ID; const timestamp = useMemo( () => formatTimeAgo(time), [time, timestampUpdates] @@ -116,6 +124,7 @@ export function ChatMessage({
- {!actionsOpen && ( + {!isDirect && !isOptimistic && !actionsOpen && (
-
)} - {actionsOpen && ( + {!isDirect && !isOptimistic && actionsOpen && (
- + {userId && parseInt(userId) !== parseInt(user_id) && ( + + )}
)} - {dm && ( + {!isDirect && dm && ( (Sent only to you) diff --git a/chat/src/hooks/useChat.tsx b/chat/src/hooks/useChat.tsx index d0fe06a67..2cffc187e 100644 --- a/chat/src/hooks/useChat.tsx +++ b/chat/src/hooks/useChat.tsx @@ -58,9 +58,11 @@ const ChatContext = createContext({ }); const MINIMUM_TYPING_UPDATE_INTERVAL = 250; +export const DIRECT_MESSAGE_ID = "DIRECT_MESSAGE"; +export const OPTIMISTIC_MESSAGE_ID = "OPTIMISTIC"; export function ChatProvider({ children }: PropsWithChildren) { - const { username, siteName } = useRootContext(); + const { username, id, siteName, hat, avatar, nameColor } = useRootContext(); const socket = useRef(null); const [online, setOnline] = useState([]); const [typing, setTyping] = useState([]); @@ -73,13 +75,69 @@ export function ChatProvider({ children }: PropsWithChildren) { const [notifications, setNotifications] = useState(0); const [messageLookup, setMessageLookup] = useState({}); const addMessage = useCallback((message: IChatMessage) => { - setMessages((prev) => [...prev.slice(-99), message]); + if (message.id === OPTIMISTIC_MESSAGE_ID) { + setMessages((prev) => prev.concat(message)); + } else { + // Are there any optimistic messages that have the same text? + setMessages((prev) => { + const matchingOptimisticMessage = prev.findIndex( + (prevMessage) => + prevMessage.id === OPTIMISTIC_MESSAGE_ID && + prevMessage.text.trim() === message.text.trim() + ); + + if (matchingOptimisticMessage === -1) { + return prev.slice(-99).concat(message); + } else { + const before = prev.slice(0, matchingOptimisticMessage); + const after = prev.slice(matchingOptimisticMessage + 1); + + return [...before, message, ...after]; + } + }); + } if (message.username !== username && !document.hasFocus()) { setNotifications((prev) => prev + 1); } }, []); const sendMessage = useCallback(() => { + if (userToDm) { + const directMessage = `(Sent to @${userToDm.username}): ${draft}`; + + addMessage({ + id: DIRECT_MESSAGE_ID, + username, + user_id: id, + avatar, + hat, + namecolor: nameColor, + text: directMessage, + base_text_censored: directMessage, + text_censored: directMessage, + text_html: directMessage, + time: new Date().getTime() / 1000, + quotes: null, + dm: true, + }); + } else { + addMessage({ + id: OPTIMISTIC_MESSAGE_ID, + username, + user_id: id, + avatar, + hat, + namecolor: nameColor, + text: draft, + base_text_censored: draft, + text_censored: draft, + text_html: draft, + time: new Date().getTime() / 1000, + quotes: null, + dm: false, + }); + } + socket.current?.emit(ChatHandlers.SPEAK, { message: draft, quotes: quote?.id ?? null, diff --git a/chat/src/hooks/useRootContext.ts b/chat/src/hooks/useRootContext.ts index 81c3409d6..c12a9909a 100644 --- a/chat/src/hooks/useRootContext.ts +++ b/chat/src/hooks/useRootContext.ts @@ -1,15 +1,30 @@ import { useEffect, useState } from "react"; export function useRootContext() { - const [{ admin, id, username, censored, themeColor, siteName }, setContext] = - useState({ - id: "", - username: "", - admin: false, - censored: true, - themeColor: "#ff66ac", - siteName: "", - }); + const [ + { + admin, + id, + username, + censored, + themeColor, + siteName, + nameColor, + avatar, + hat, + }, + setContext, + ] = useState({ + id: "", + username: "", + admin: false, + censored: true, + themeColor: "#ff66ac", + siteName: "", + nameColor: "", + avatar: "", + hat: "", + }); useEffect(() => { const root = document.getElementById("root"); @@ -21,8 +36,21 @@ export function useRootContext() { censored: root.dataset.censored === "True", themeColor: root.dataset.themecolor, siteName: root.dataset.sitename, + nameColor: root.dataset.namecolor, + avatar: root.dataset.avatar, + hat: root.dataset.hat, }); }, []); - return { id, admin, username, censored, themeColor, siteName }; + return { + id, + admin, + username, + censored, + themeColor, + siteName, + nameColor, + avatar, + hat, + }; } diff --git a/files/templates/chat.html b/files/templates/chat.html index 013b84bf3..6e716725c 100644 --- a/files/templates/chat.html +++ b/files/templates/chat.html @@ -35,7 +35,10 @@ data-admin="{{v.admin_level > 1}}" data-censored="{{v.slurreplacer}}" data-sitename="{{SITE_NAME}}" - data-themecolor="{{v.themecolor}}"> + data-themecolor="{{v.themecolor}}" + data-namecolor="{{v.namecolor}}" + data-avatar="{{v.profile_url}}" + data-hat="{{v.hat_active}}">