Add DMing to chat
parent
77ed5728c4
commit
300a5164f6
|
@ -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 {
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 && (
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue