Add DMing to chat

remotes/1693176582716663532/tmp_refs/heads/watchparty
Outrun Colors 2022-09-27 00:15:22 -05:00
parent 77ed5728c4
commit 300a5164f6
No known key found for this signature in database
GPG Key ID: 0426976DCEFE6073
8 changed files with 129 additions and 12 deletions

2
chat/global.d.ts vendored
View File

@ -5,6 +5,7 @@ declare var process: {
declare interface IChatMessage { declare interface IChatMessage {
id: string; id: string;
username: string; username: string;
user_id?: string;
avatar: string; avatar: string;
hat: string; hat: string;
namecolor: string; namecolor: string;
@ -14,6 +15,7 @@ declare interface IChatMessage {
text_html: string; text_html: string;
time: number; time: number;
quotes: null | string; quotes: null | string;
dm: boolean;
} }
declare interface EmojiModSelection { declare interface EmojiModSelection {

View File

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

View File

@ -36,7 +36,7 @@ function AppInner() {
const { open, config } = useDrawer(); const { open, config } = useDrawer();
const contentWrapper = useRef<HTMLDivElement>(null); const contentWrapper = useRef<HTMLDivElement>(null);
const initiallyScrolledDown = useRef(false); const initiallyScrolledDown = useRef(false);
const { messages, quote } = useChat(); const { messages, quote, userToDm, updateUserToDm } = useChat();
// See: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ // See: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
useEffect(() => { useEffect(() => {
@ -120,6 +120,25 @@ function AppInner() {
<QuotedMessage /> <QuotedMessage />
</div> </div>
)} )}
{userToDm && (
<div
className="App-bottom-extra text-primary"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<em>Directly messaging @{userToDm.username}</em>
<button
type="button"
className="btn btn-secondary"
onClick={() => updateUserToDm(null)}
>
Cancel
</button>
</div>
)}
<UserInput /> <UserInput />
<UsersTyping /> <UsersTyping />
</div> </div>

View File

@ -1,7 +1,12 @@
.ChatMessage { .ChatMessage {
position: relative; position: relative;
padding-right: 1.5rem; padding-right: 1.5rem;
min-height: 28px; }
.ChatMessage__isDm {
background: var(--gray-800);
border-top: 1px dashed var(--primary);
border-bottom: 1px dashed var(--primary);
} }
.ChatMessage p { .ChatMessage p {

View File

@ -43,6 +43,7 @@ export function ChatMessage({
text_censored, text_censored,
time, time,
quotes, quotes,
dm,
} = message; } = message;
const { const {
id: userId, id: userId,
@ -51,7 +52,14 @@ export function ChatMessage({
censored, censored,
themeColor, themeColor,
} = useRootContext(); } = useRootContext();
const { messageLookup, deleteMessage, quoteMessage } = useChat(); const {
messageLookup,
userToDm,
quote,
deleteMessage,
quoteMessage,
updateUserToDm,
} = useChat();
const [confirmedDelete, setConfirmedDelete] = useState(false); const [confirmedDelete, setConfirmedDelete] = useState(false);
const quotedMessage = messageLookup[quotes]; const quotedMessage = messageLookup[quotes];
const content = censored ? text_censored : text_html; const content = censored ? text_censored : text_html;
@ -72,9 +80,31 @@ export function ChatMessage({
} }
}, [text, confirmedDelete]); }, [text, confirmedDelete]);
const handleQuoteMessageAction = useCallback(() => { const handleQuoteMessageAction = useCallback(() => {
updateUserToDm(null);
quoteMessage(message); quoteMessage(message);
onToggleActions(message.id); onToggleActions(message.id);
}, [message, onToggleActions]); }, [message, onToggleActions]);
const handleDirectMessage = useCallback(
(toggle?: boolean) => {
const userId = message.user_id ?? "";
if (userToDm && userToDm.id === userId) {
updateUserToDm(null);
} else {
updateUserToDm({
id: userId,
username: message.username,
});
quoteMessage(null);
if (toggle) {
onToggleActions(message.id);
}
}
},
[userToDm, message.id, message.user_id, message.username]
);
useEffect(() => { useEffect(() => {
if (!actionsOpen) { if (!actionsOpen) {
@ -85,12 +115,11 @@ export function ChatMessage({
return ( return (
<div <div
className={cx("ChatMessage", { className={cx("ChatMessage", {
ChatMessage__showingUser: showUser, ChatMessage__isDm: dm,
ChatMessage__isMention: isMention,
})} })}
id={id} id={id}
style={ style={
isMention isMention && !dm
? { ? {
background: `#${themeColor}25`, background: `#${themeColor}25`,
borderLeft: `1px solid #${themeColor}`, borderLeft: `1px solid #${themeColor}`,
@ -102,7 +131,21 @@ export function ChatMessage({
<div className="ChatMessage-actions-button"> <div className="ChatMessage-actions-button">
<button <button
className="btn btn-secondary" className="btn btn-secondary"
onClick={() => quoteMessage(message)} style={{
position: "relative",
top: 2,
left: -12
}}
onClick={() => handleDirectMessage()}
>
📨
</button>
<button
className="btn btn-secondary"
onClick={() => {
updateUserToDm(null);
quoteMessage(quote ? null : message);
}}
> >
<i className="fas fa-reply" /> <i className="fas fa-reply" />
</button> </button>
@ -116,6 +159,12 @@ export function ChatMessage({
)} )}
{actionsOpen && ( {actionsOpen && (
<div className="ChatMessage-actions"> <div className="ChatMessage-actions">
<button
className="btn btn-secondary ChatMessage-button"
onClick={() => handleDirectMessage(true)}
>
📨 DM @{message.username}
</button>
<button <button
className="btn btn-secondary ChatMessage-button" className="btn btn-secondary ChatMessage-button"
onClick={handleQuoteMessageAction} onClick={handleQuoteMessageAction}
@ -157,6 +206,11 @@ export function ChatMessage({
<QuotedMessageLink message={quotedMessage} /> <QuotedMessageLink message={quotedMessage} />
</div> </div>
)} )}
{dm && (
<small className="ChatMessage-quoted-link text-primary">
<em>(Sent only to you)</em>
</small>
)}
<div className="ChatMessage-bottom"> <div className="ChatMessage-bottom">
<div> <div>
<span <span

View File

@ -13,7 +13,7 @@ import { EmojiDrawer, QuickEmojis } from "../emoji";
import "./UserInput.css"; import "./UserInput.css";
export function UserInput() { export function UserInput() {
const { draft, sendMessage, updateDraft } = useChat(); const { draft, userToDm, sendMessage, updateDraft } = useChat();
const builtChatInput = useRef<HTMLTextAreaElement>(null); const builtChatInput = useRef<HTMLTextAreaElement>(null);
const { visible, addQuery } = useEmojis(); const { visible, addQuery } = useEmojis();
const form = useRef<HTMLFormElement>(null); const form = useRef<HTMLFormElement>(null);
@ -81,6 +81,12 @@ export function UserInput() {
} }
}, []); }, []);
useEffect(() => {
if (userToDm) {
builtChatInput.current?.focus();
}
}, [userToDm])
return ( return (
<form ref={form} className="UserInput" onSubmit={handleSendMessage}> <form ref={form} className="UserInput" onSubmit={handleSendMessage}>
{quickEmojis.length > 0 && ( {quickEmojis.length > 0 && (

View File

@ -22,6 +22,11 @@ enum ChatHandlers {
SPEAK = "speak", SPEAK = "speak",
} }
interface UserToDM {
id: string;
username: string;
}
interface ChatProviderContext { interface ChatProviderContext {
online: string[]; online: string[];
typing: string[]; typing: string[];
@ -29,10 +34,12 @@ interface ChatProviderContext {
draft: string; draft: string;
quote: null | IChatMessage; quote: null | IChatMessage;
messageLookup: Record<string, IChatMessage>; messageLookup: Record<string, IChatMessage>;
userToDm: null | UserToDM;
updateDraft: React.Dispatch<React.SetStateAction<string>>; updateDraft: React.Dispatch<React.SetStateAction<string>>;
sendMessage(): void; sendMessage(): void;
quoteMessage(message: null | IChatMessage): void; quoteMessage(message: null | IChatMessage): void;
deleteMessage(withText: string): void; deleteMessage(withText: string): void;
updateUserToDm(userToDm: UserToDM): void;
} }
const ChatContext = createContext<ChatProviderContext>({ const ChatContext = createContext<ChatProviderContext>({
@ -42,10 +49,12 @@ const ChatContext = createContext<ChatProviderContext>({
draft: "", draft: "",
quote: null, quote: null,
messageLookup: {}, messageLookup: {},
userToDm: null,
updateDraft() {}, updateDraft() {},
sendMessage() {}, sendMessage() {},
quoteMessage() {}, quoteMessage() {},
deleteMessage() {}, deleteMessage() {},
updateUserToDm() {},
}); });
const MINIMUM_TYPING_UPDATE_INTERVAL = 250; const MINIMUM_TYPING_UPDATE_INTERVAL = 250;
@ -60,6 +69,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
const lastDraft = useRef(""); const lastDraft = useRef("");
const [quote, setQuote] = useState<null | IChatMessage>(null); const [quote, setQuote] = useState<null | IChatMessage>(null);
const focused = useWindowFocus(); const focused = useWindowFocus();
const [userToDm, setUserToDm] = useState<null | UserToDM>(null);
const [notifications, setNotifications] = useState<number>(0); const [notifications, setNotifications] = useState<number>(0);
const [messageLookup, setMessageLookup] = useState({}); const [messageLookup, setMessageLookup] = useState({});
const addMessage = useCallback((message: IChatMessage) => { const addMessage = useCallback((message: IChatMessage) => {
@ -73,11 +83,13 @@ export function ChatProvider({ children }: PropsWithChildren) {
socket.current?.emit(ChatHandlers.SPEAK, { socket.current?.emit(ChatHandlers.SPEAK, {
message: draft, message: draft,
quotes: quote?.id ?? null, quotes: quote?.id ?? null,
recipient: userToDm?.id ?? "",
}); });
setQuote(null); setQuote(null);
setDraft(""); setDraft("");
}, [draft, quote]); setUserToDm(null);
}, [draft, quote, userToDm]);
const requestDeleteMessage = useCallback((withText: string) => { const requestDeleteMessage = useCallback((withText: string) => {
socket.current?.emit(ChatHandlers.DELETE, withText); socket.current?.emit(ChatHandlers.DELETE, withText);
}, []); }, []);
@ -105,10 +117,12 @@ export function ChatProvider({ children }: PropsWithChildren) {
draft, draft,
quote, quote,
messageLookup, messageLookup,
userToDm,
quoteMessage, quoteMessage,
sendMessage, sendMessage,
deleteMessage: requestDeleteMessage, deleteMessage: requestDeleteMessage,
updateDraft: setDraft, updateDraft: setDraft,
updateUserToDm: setUserToDm,
}), }),
[ [
online, online,
@ -117,6 +131,7 @@ export function ChatProvider({ children }: PropsWithChildren) {
draft, draft,
quote, quote,
messageLookup, messageLookup,
userToDm,
sendMessage, sendMessage,
deleteMessage, deleteMessage,
quoteMessage, quoteMessage,

View File

@ -6,10 +6,9 @@ from files.helpers.sanitize import sanitize
from files.helpers.const import * from files.helpers.const import *
from files.helpers.alerts import * from files.helpers.alerts import *
from files.helpers.regex import * from files.helpers.regex import *
from datetime import datetime
from flask_socketio import SocketIO, emit from flask_socketio import SocketIO, emit
from files.__main__ import app, limiter, cache from files.__main__ import app, limiter, cache
from flask import render_template, make_response, send_from_directory from flask import render_template
import sys import sys
import atexit import atexit
@ -33,6 +32,8 @@ cache.set(ONLINE_STR, len(online), timeout=0)
muted = cache.get(f'{SITE}_muted') or {} muted = cache.get(f'{SITE}_muted') or {}
messages = cache.get(f'{SITE}_chat') or [] messages = cache.get(f'{SITE}_chat') or []
total = cache.get(f'{SITE}_total') or 0 total = cache.get(f'{SITE}_total') or 0
socket_ids_to_user_ids = {}
user_ids_to_socket_ids = {}
@app.get("/chat") @app.get("/chat")
@is_not_permabanned @is_not_permabanned
@ -60,11 +61,14 @@ def speak(data, v):
if not text: return '', 403 if not text: return '', 403
text_html = sanitize(text, count_marseys=True) text_html = sanitize(text, count_marseys=True)
quotes = data['quotes'] quotes = data['quotes']
recipient = data['recipient']
data={ data={
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"quotes": quotes, "quotes": quotes,
"avatar": v.profile_url, "avatar": v.profile_url,
"hat": v.hat_active, "hat": v.hat_active,
"user_id": v.id,
"dm": bool(recipient and recipient != ""),
"username": v.username, "username": v.username,
"namecolor": v.name_color, "namecolor": v.name_color,
"text": text, "text": text,
@ -81,6 +85,9 @@ def speak(data, v):
v.shadowbanned = 'AutoJanny' v.shadowbanned = 'AutoJanny'
g.db.add(v) g.db.add(v)
send_repeatable_notification(CARP_ID, f"{v.username} has been shadowbanned because of a chat message.") send_repeatable_notification(CARP_ID, f"{v.username} has been shadowbanned because of a chat message.")
elif recipient and user_ids_to_socket_ids.get(recipient):
recipient_sid = user_ids_to_socket_ids[recipient]
emit('speak', data, broadcast=False, to=recipient_sid)
else: else:
emit('speak', data, broadcast=True) emit('speak', data, broadcast=True)
messages.append(data) messages.append(data)
@ -106,6 +113,10 @@ def connect(v):
emit("online", online, broadcast=True) emit("online", online, broadcast=True)
cache.set(ONLINE_STR, len(online), timeout=0) cache.set(ONLINE_STR, len(online), timeout=0)
if not socket_ids_to_user_ids.get(request.sid):
socket_ids_to_user_ids[request.sid] = v.id
user_ids_to_socket_ids[v.id] = request.sid
emit('online', online) emit('online', online)
emit('catchup', messages) emit('catchup', messages)
emit('typing', typing) emit('typing', typing)
@ -120,6 +131,11 @@ def disconnect(v):
cache.set(ONLINE_STR, len(online), timeout=0) cache.set(ONLINE_STR, len(online), timeout=0)
if v.username in typing: typing.remove(v.username) if v.username in typing: typing.remove(v.username)
if socket_ids_to_user_ids.get(request.sid):
del socket_ids_to_user_ids[request.sid]
del user_ids_to_socket_ids[v.id]
emit('typing', typing, broadcast=True) emit('typing', typing, broadcast=True)
return '', 204 return '', 204