Add DMing to chat
parent
77ed5728c4
commit
300a5164f6
|
@ -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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chat",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"main": "./src/index.tsx",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 && (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue