Add optimistic messages
parent
2073ef9dbb
commit
8ac70df325
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chat",
|
||||
"version": "0.1.26",
|
||||
"version": "0.1.27",
|
||||
"main": "./src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
border-bottom: 1px dashed var(--primary);
|
||||
}
|
||||
|
||||
.ChatMessage__isOptimistic {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ChatMessage p {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -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({
|
|||
<div
|
||||
className={cx("ChatMessage", {
|
||||
ChatMessage__isDm: dm,
|
||||
ChatMessage__isOptimistic: isOptimistic,
|
||||
})}
|
||||
id={id}
|
||||
style={
|
||||
|
@ -127,19 +136,8 @@ export function ChatMessage({
|
|||
: {}
|
||||
}
|
||||
>
|
||||
{!actionsOpen && (
|
||||
{!isDirect && !isOptimistic && !actionsOpen && (
|
||||
<div className="ChatMessage-actions-button">
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
style={{
|
||||
position: "relative",
|
||||
top: 2,
|
||||
left: -12,
|
||||
}}
|
||||
onClick={() => handleDirectMessage()}
|
||||
>
|
||||
📨
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
onClick={() => {
|
||||
|
@ -157,14 +155,16 @@ export function ChatMessage({
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
{actionsOpen && (
|
||||
{!isDirect && !isOptimistic && actionsOpen && (
|
||||
<div className="ChatMessage-actions">
|
||||
<button
|
||||
className="btn btn-secondary ChatMessage-button"
|
||||
onClick={() => handleDirectMessage(true)}
|
||||
>
|
||||
📨 DM @{message.username}
|
||||
</button>
|
||||
{userId && parseInt(userId) !== parseInt(user_id) && (
|
||||
<button
|
||||
className="btn btn-secondary ChatMessage-button"
|
||||
onClick={() => handleDirectMessage(true)}
|
||||
>
|
||||
📨 DM @{message.username}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-secondary ChatMessage-button"
|
||||
onClick={handleQuoteMessageAction}
|
||||
|
@ -206,7 +206,7 @@ export function ChatMessage({
|
|||
<QuotedMessageLink message={quotedMessage} />
|
||||
</div>
|
||||
)}
|
||||
{dm && (
|
||||
{!isDirect && dm && (
|
||||
<small className="ChatMessage-quoted-link text-primary">
|
||||
<em>(Sent only to you)</em>
|
||||
</small>
|
||||
|
|
|
@ -58,9 +58,11 @@ const ChatContext = createContext<ChatProviderContext>({
|
|||
});
|
||||
|
||||
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 | Socket>(null);
|
||||
const [online, setOnline] = useState<string[]>([]);
|
||||
const [typing, setTyping] = useState<string[]>([]);
|
||||
|
@ -73,13 +75,69 @@ export function ChatProvider({ children }: PropsWithChildren) {
|
|||
const [notifications, setNotifications] = useState<number>(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 = `<small class="text-primary"><em>(Sent to @${userToDm.username}):</em></small> ${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,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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}}">
|
||||
</div>
|
||||
<script>window.global = window</script>
|
||||
<script defer src="{{'js/chat_done.js' | asset}}"></script>
|
||||
|
|
Loading…
Reference in New Issue