forked from rDrama/rDrama
Add typing debounce/enable quoting
parent
ee204817ae
commit
695159052b
|
@ -2,7 +2,8 @@ declare var process: {
|
|||
env: Record<string, any>;
|
||||
};
|
||||
|
||||
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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chat",
|
||||
"version": "0.0.9",
|
||||
"version": "0.0.10",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 (
|
||||
<div className="Activity">
|
||||
{ACTIVITIES.map((activity) => (
|
||||
<section key={activity.title}>
|
||||
<i className={`fas fa-${activity.icon}`}></i>
|
||||
<h4>{activity.title}</h4>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from "./Activity";
|
|
@ -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%;
|
||||
}
|
|
@ -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 (
|
||||
<section className="Chat" id="chatWrapper">
|
||||
<div className="Chat-window">
|
||||
<div className="Chat-mobile-top">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() =>
|
||||
reveal({
|
||||
title: "Users in chat",
|
||||
content: (
|
||||
<div className="Chat-drawer">
|
||||
<UserList fluid={true} />
|
||||
</div>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
<i className="far fa-user" /> <span>{online.length}</span>
|
||||
</button>
|
||||
</div>
|
||||
<ChatMessageList />
|
||||
{quote && <QuotedMessage />}
|
||||
<UserInput />
|
||||
</div>
|
||||
<div className="Chat-side">
|
||||
<UserList />
|
||||
{process.env.FEATURES_ACTIVITY && <ActivityList />}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
|
@ -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 (
|
||||
<div className={"ChatMessage"} style={style}>
|
||||
<div className="ChatMessage" style={mentionStyle} id={id}>
|
||||
{showUser && (
|
||||
<div className="ChatMessage-top">
|
||||
<Username
|
||||
|
@ -84,6 +86,45 @@ export function ChatMessage({
|
|||
<div className="ChatMessage-timestamp">{timestamp}</div>
|
||||
</div>
|
||||
)}
|
||||
{quotes && (
|
||||
<a
|
||||
href="#"
|
||||
onClick={() => {
|
||||
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}:{" "}
|
||||
<em>
|
||||
"
|
||||
{messageLookup[quotes]?.text.slice(
|
||||
0,
|
||||
QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH
|
||||
)}
|
||||
{messageLookup[quotes]?.text.length >=
|
||||
QUOTED_MESSAGE_CONTEXTUAL_SNIPPET_LENGTH
|
||||
? "..."
|
||||
: ""}
|
||||
"
|
||||
</em>
|
||||
</a>
|
||||
)}
|
||||
<div className="ChatMessage-bottom">
|
||||
<div>
|
||||
<span
|
||||
|
|
|
@ -12,7 +12,7 @@ import { EmojiDrawer, QuickEmojis } from "../emoji";
|
|||
import "./UserInput.css";
|
||||
|
||||
export function UserInput() {
|
||||
const { draft, sendMessage, updateDraft } = useChat();
|
||||
const { messages, draft, sendMessage, updateDraft } = useChat();
|
||||
const { reveal, hide, open } = useDrawer();
|
||||
const builtChatInput = useRef<HTMLTextAreaElement>(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
|
||||
</span>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
export * from "./ActivityList";
|
||||
export * from "./Chat";
|
||||
export * from "./ChatHeading";
|
||||
export * from "./ChatMessage";
|
||||
export * from "./QuotedMessage";
|
||||
|
|
|
@ -1,3 +1,2 @@
|
|||
export * from "./activity";
|
||||
export * from "./chat";
|
||||
export * from "./emoji";
|
||||
|
|
|
@ -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<string, IChatMessage>;
|
||||
updateDraft: React.Dispatch<React.SetStateAction<string>>;
|
||||
sendMessage(): void;
|
||||
quoteMessage(message: null | ChatSpeakResponse): void;
|
||||
quoteMessage(message: null | IChatMessage): void;
|
||||
deleteMessage(withText: string): void;
|
||||
}
|
||||
|
||||
|
@ -40,23 +42,28 @@ const ChatContext = createContext<ChatProviderContext>({
|
|||
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 | Socket>(null);
|
||||
const [online, setOnline] = useState<string[]>([]);
|
||||
const [typing, setTyping] = useState<string[]>([]);
|
||||
const [messages, setMessages] = useState<ChatSpeakResponse[]>([]);
|
||||
const [messages, setMessages] = useState<IChatMessage[]>([]);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [quote, setQuote] = useState<null | ChatSpeakResponse>(null);
|
||||
const lastDraft = useRef("");
|
||||
const [quote, setQuote] = useState<null | IChatMessage>(null);
|
||||
const focused = useWindowFocus();
|
||||
const [notifications, setNotifications] = useState<number>(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()) {
|
||||
|
@ -64,14 +71,14 @@ export function ChatProvider({ children }: PropsWithChildren) {
|
|||
}
|
||||
}, []);
|
||||
const sendMessage = useCallback(() => {
|
||||
const message = quote
|
||||
? `> ${quote.text}\n@${quote.username}<br /><br />${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<string, IChatMessage>)
|
||||
);
|
||||
}, [messages]);
|
||||
|
||||
// Display e.g. [+2] Chat when notifications occur when you're away.
|
||||
useEffect(() => {
|
||||
const title = document.getElementsByTagName("title")[0];
|
||||
|
|
|
@ -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)}
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue