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}}">