Add optimistic messages

remotes/1693176582716663532/tmp_refs/heads/watchparty
Outrun Colors 2022-09-27 19:06:44 -05:00
parent 2073ef9dbb
commit 8ac70df325
6 changed files with 128 additions and 35 deletions

View File

@ -1,6 +1,6 @@
{
"name": "chat",
"version": "0.1.26",
"version": "0.1.27",
"main": "./src/index.tsx",
"license": "MIT",
"dependencies": {

View File

@ -9,6 +9,10 @@
border-bottom: 1px dashed var(--primary);
}
.ChatMessage__isOptimistic {
opacity: 0.5;
}
.ChatMessage p {
margin: 0;
}

View File

@ -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>

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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>