Change profanity filter to reject messages with profanity instead of censoring

This commit is contained in:
2025-11-27 22:50:34 +03:00
Unverified
parent add674487c
commit 620c1db260
10 changed files with 302 additions and 268 deletions
+32 -3
View File
@@ -3,6 +3,18 @@ import { getAuthHeaders } from "./account";
import type { Message, Messages, SendMessageRequest } from "@/core/types";
import { request } from "@/core/websocket";
class HttpError extends Error {
status: number;
detail: string;
constructor(message: string, status: number, detail: string) {
super(message);
this.name = "HttpError";
this.status = status;
this.detail = detail;
}
}
/**
* Fetches public chat messages
*/
@@ -57,8 +69,15 @@ export async function sendMessageWithFiles(
body: form
});
if (!res.ok) {
const error = await res.text();
throw new Error(error || "Failed to send message with files");
let errorDetail = "Failed to send message with files";
try {
const errorJson = await res.json();
errorDetail = errorJson.detail || errorDetail;
} catch {
const errorText = await res.text();
errorDetail = errorText || errorDetail;
}
throw new HttpError(errorDetail, res.status, errorDetail);
}
}
@@ -71,7 +90,17 @@ export async function editMessage(messageId: number, newContent: string, authTok
headers: getAuthHeaders(authToken, true),
body: JSON.stringify({ content: newContent })
});
if (!res.ok) throw new Error("Failed to edit message");
if (!res.ok) {
let errorDetail = "Failed to edit message";
try {
const errorJson = await res.json();
errorDetail = errorJson.detail || errorDetail;
} catch {
const errorText = await res.text();
errorDetail = errorText || errorDetail;
}
throw new HttpError(errorDetail, res.status, errorDetail);
}
}
/**
@@ -0,0 +1,94 @@
import { useState, useCallback, useEffect } from "react";
import { StyledDialog } from "./StyledDialog";
import { MaterialButton } from "@/utils/material";
import styles from "./css/alert-dialog.module.scss";
interface AlertDialogState {
open: boolean;
message: string;
resolve: (() => void) | null;
}
let alertState: AlertDialogState = {
open: false,
message: "",
resolve: null
};
const listeners = new Set<() => void>();
function notifyListeners() {
listeners.forEach(listener => listener());
}
/**
* Drop-in replacement for window.alert() using StyledDialog
* @param message - The message to display
* @returns Promise that resolves when the dialog is closed
*/
export function alert(message: string): Promise<void> {
return new Promise<void>((resolve) => {
alertState = {
open: true,
message,
resolve: () => {
alertState.open = false;
alertState.message = "";
alertState.resolve = null;
notifyListeners();
resolve();
}
};
notifyListeners();
});
}
/**
* Internal component that renders the alert dialog
*/
export function AlertDialogProvider() {
const [, setUpdateKey] = useState(0);
const update = useCallback(() => {
setUpdateKey(prev => prev + 1);
}, []);
useEffect(() => {
listeners.add(update);
return () => {
listeners.delete(update);
};
}, [update]);
const handleClose = () => {
if (alertState.resolve) {
alertState.resolve();
}
};
return (
<StyledDialog
open={alertState.open}
onOpenChange={(open) => {
if (!open) {
handleClose();
}
}}
onBackdropClick={handleClose}
className={styles.alertDialog}
contentClassName={styles.alertDialogContent}
>
<div className={styles.alertDialogMessage}>
{alertState.message}
</div>
<div className={styles.alertDialogActions}>
<MaterialButton
variant="filled"
onClick={handleClose}
>
OK
</MaterialButton>
</div>
</StyledDialog>
);
}
@@ -0,0 +1,25 @@
@use "../../../css/colors" as *;
@use "../../../css/material" as *;
.alertDialog {
.alertDialogContent {
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.alertDialogMessage {
color: $color-dark-on-surface;
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.alertDialogActions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
}
+24 -3
View File
@@ -15,6 +15,11 @@ import { useUserStore } from "@/state/user";
import { getLastSequence, processBatchedUpdates, requestMissedUpdates } from "./updateManager";
import { getAuthToken } from "@/core/api/user/auth";
interface HttpError extends Error {
status?: number;
detail?: string;
}
/**
* Creates a new WebSocket connection to the chat server
* @returns {WebSocket} New WebSocket instance
@@ -303,13 +308,29 @@ export function request<Request, Response = any>(payload: WebSocketMessage<Reque
}
const listener = (e: MessageEvent) => {
clearTimeout(timeoutId);
try {
resolve(JSON.parse(e.data));
const response = JSON.parse(e.data);
// Only handle responses that match our request type or have an error
if (response.type === payload.type || response.error) {
clearTimeout(timeoutId);
websocket.removeEventListener("message", listener);
// Check if the response contains an error field
if (response.error) {
const error = new Error(response.error.detail || "WebSocket request failed");
(error as HttpError).status = response.error.code;
(error as HttpError).detail = response.error.detail || "";
reject(error);
} else {
resolve(response);
}
}
// If it doesn't match, let other handlers process it
} catch (error) {
clearTimeout(timeoutId);
websocket.removeEventListener("message", listener);
reject(error);
}
websocket.removeEventListener("message", listener);
};
websocket.addEventListener("message", listener);