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

View File

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

View File

@ -36,7 +36,7 @@ function AppInner() {
const { open, config } = useDrawer();
const contentWrapper = useRef<HTMLDivElement>(null);
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/
useEffect(() => {
@ -120,6 +120,25 @@ function AppInner() {
<QuotedMessage />
</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 />
<UsersTyping />
</div>

View File

@ -1,7 +1,12 @@
.ChatMessage {
position: relative;
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 {

View File

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

View File

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

View File

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

View File

@ -6,10 +6,9 @@ from files.helpers.sanitize import sanitize
from files.helpers.const import *
from files.helpers.alerts import *
from files.helpers.regex import *
from datetime import datetime
from flask_socketio import SocketIO, emit
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 atexit
@ -33,6 +32,8 @@ cache.set(ONLINE_STR, len(online), timeout=0)
muted = cache.get(f'{SITE}_muted') or {}
messages = cache.get(f'{SITE}_chat') or []
total = cache.get(f'{SITE}_total') or 0
socket_ids_to_user_ids = {}
user_ids_to_socket_ids = {}
@app.get("/chat")
@is_not_permabanned
@ -60,11 +61,14 @@ def speak(data, v):
if not text: return '', 403
text_html = sanitize(text, count_marseys=True)
quotes = data['quotes']
recipient = data['recipient']
data={
"id": str(uuid.uuid4()),
"quotes": quotes,
"avatar": v.profile_url,
"hat": v.hat_active,
"user_id": v.id,
"dm": bool(recipient and recipient != ""),
"username": v.username,
"namecolor": v.name_color,
"text": text,
@ -81,6 +85,9 @@ def speak(data, v):
v.shadowbanned = 'AutoJanny'
g.db.add(v)
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:
emit('speak', data, broadcast=True)
messages.append(data)
@ -106,6 +113,10 @@ def connect(v):
emit("online", online, broadcast=True)
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('catchup', messages)
emit('typing', typing)
@ -120,6 +131,11 @@ def disconnect(v):
cache.set(ONLINE_STR, len(online), timeout=0)
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)
return '', 204