mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Change profanity filter to reject messages with profanity instead of censoring
This commit is contained in:
@@ -8,6 +8,7 @@ import NotFoundPage from "./pages/not-found/NotFoundPage";
|
||||
import ProtectedRoute from "./pages/ProtectedRoute";
|
||||
import DownloadAppPage from "./pages/download-app/DownloadAppPage";
|
||||
import { SuspensionDialog } from "./pages/chat/ui/SuspensionDialog";
|
||||
import { AlertDialogProvider } from "./core/components/AlertDialog";
|
||||
import { delay } from "./utils/utils";
|
||||
|
||||
// Lazy load route components
|
||||
@@ -129,6 +130,7 @@ export default function App() {
|
||||
return authReady && (
|
||||
<BrowserRouter>
|
||||
<ElectronTitleBar />
|
||||
<AlertDialogProvider />
|
||||
<div id="main-wrapper">
|
||||
<AnimatedRoutes />
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -184,34 +184,30 @@ export class DMPanel extends MessagePanel {
|
||||
protected async sendMessage(content: string, replyToId?: number, files: File[] = []): Promise<void> {
|
||||
if (!this.currentUser.authToken || !this.dmData || !content.trim()) return;
|
||||
|
||||
try {
|
||||
const payload: DmEncryptedJSON = {
|
||||
type: "text",
|
||||
data: {
|
||||
content: content.trim(),
|
||||
reply_to_id: replyToId ?? undefined
|
||||
}
|
||||
const payload: DmEncryptedJSON = {
|
||||
type: "text",
|
||||
data: {
|
||||
content: content.trim(),
|
||||
reply_to_id: replyToId ?? undefined
|
||||
}
|
||||
const json = JSON.stringify(payload);
|
||||
}
|
||||
const json = JSON.stringify(payload);
|
||||
|
||||
if (files.length === 0) {
|
||||
await api.chats.dm.send(
|
||||
this.dmData.userId,
|
||||
this.dmData.publicKey,
|
||||
json,
|
||||
this.currentUser.authToken
|
||||
);
|
||||
} else {
|
||||
await api.chats.dm.sendWithFiles(
|
||||
this.dmData.userId,
|
||||
this.dmData.publicKey,
|
||||
json,
|
||||
files,
|
||||
this.currentUser.authToken
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to send DM:", error);
|
||||
if (files.length === 0) {
|
||||
await api.chats.dm.send(
|
||||
this.dmData.userId,
|
||||
this.dmData.publicKey,
|
||||
json,
|
||||
this.currentUser.authToken
|
||||
);
|
||||
} else {
|
||||
await api.chats.dm.sendWithFiles(
|
||||
this.dmData.userId,
|
||||
this.dmData.publicKey,
|
||||
json,
|
||||
files,
|
||||
this.currentUser.authToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import type { Message, WebSocketMessage } from "@/core/types";
|
||||
import type { UserState, ProfileDialogData } from "@/state/types";
|
||||
import { alert } from "@/core/components/AlertDialog";
|
||||
|
||||
interface HttpError extends Error {
|
||||
status?: number;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface MessagePanelState {
|
||||
id: string;
|
||||
@@ -50,7 +56,7 @@ export abstract class MessagePanel {
|
||||
abstract loadMessages(): Promise<void>;
|
||||
protected abstract sendMessage(content: string, replyToId?: number, files?: File[]): Promise<void>;
|
||||
abstract isDm(): boolean;
|
||||
abstract handleWebSocketMessage(response: WebSocketMessage<any>): Promise<void>;
|
||||
abstract handleWebSocketMessage(response: WebSocketMessage<unknown>): Promise<void>;
|
||||
abstract getProfile(): Promise<ProfileDialogData | null>;
|
||||
|
||||
// Common methods
|
||||
@@ -91,7 +97,7 @@ export abstract class MessagePanel {
|
||||
});
|
||||
}
|
||||
|
||||
protected updateMessageReactions(messageId: number, reactions: any[]): void {
|
||||
protected updateMessageReactions(messageId: number, reactions: Message["reactions"]): void {
|
||||
this.updateState({
|
||||
messages: this.state.messages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, reactions } : msg
|
||||
@@ -332,7 +338,30 @@ export abstract class MessagePanel {
|
||||
// Message sent successfully - will be updated when WebSocket confirms
|
||||
} catch (error) {
|
||||
console.error("Failed to send message:", error);
|
||||
this.handleMessageFailed(tempId);
|
||||
// Remove the temporary message from display
|
||||
this.updateState({
|
||||
messages: this.state.messages.filter(msg =>
|
||||
msg.runtimeData?.sendingState?.tempId !== tempId
|
||||
)
|
||||
});
|
||||
this.pendingMessages.delete(tempId);
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// Check if error has HTTP status code
|
||||
const httpError = error as HttpError;
|
||||
const httpStatus = httpError.status;
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
console.log("Error details:", { httpStatus, errorMessage, error });
|
||||
|
||||
// Check for profanity error: HTTP 422 status (Unprocessable Entity)
|
||||
// Also check error message as fallback for WebSocket errors
|
||||
if (httpStatus === 422 || errorMessage.includes("inappropriate content")) {
|
||||
console.log("Showing profanity error dialog");
|
||||
void alert("Your message contains inappropriate content and cannot be sent.");
|
||||
} else {
|
||||
console.log("Error does not match profanity condition:", { httpStatus, errorMessage });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,11 +370,6 @@ export abstract class MessagePanel {
|
||||
this.updateMessageToFailed(tempId);
|
||||
}
|
||||
|
||||
// Handle message send failure
|
||||
private handleMessageFailed(tempId: string): void {
|
||||
this.updateMessageToFailed(tempId);
|
||||
}
|
||||
|
||||
// Helper method to update message to failed state
|
||||
private updateMessageToFailed(tempId: string): void {
|
||||
const pending = this.pendingMessages.get(tempId);
|
||||
|
||||
@@ -90,14 +90,10 @@ export class PublicChatPanel extends MessagePanel {
|
||||
protected async sendMessage(content: string, replyToId?: number, files: File[] = []): Promise<void> {
|
||||
if (!this.currentUser.authToken || !content.trim()) return;
|
||||
|
||||
try {
|
||||
if (files.length === 0) {
|
||||
await api.chats.general.send(content, replyToId ?? null, this.currentUser.authToken);
|
||||
} else {
|
||||
await api.chats.general.sendWithFiles(content, replyToId ?? null, files, this.currentUser.authToken);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error sending message:", error);
|
||||
if (files.length === 0) {
|
||||
await api.chats.general.send(content, replyToId ?? null, this.currentUser.authToken);
|
||||
} else {
|
||||
await api.chats.general.sendWithFiles(content, replyToId ?? null, files, this.currentUser.authToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user