-
-
+ return (
+
{
+ if (!open) {
+ handleClose();
+ }
+ }}
+ onBackdropClick={handleClose}
+ >
+
-
+
+
+
+
{errors.display_name && (
{errors.display_name}
)}
- {(currentData?.userId || currentData?.isOwnProfile) && (
+ {(currentData?.userId || currentData?.isOwnProfile) && !currentData.deleted && (
)}
-
-
+ {/* Admin Actions Section - Hide for deleted users */}
+ {!currentData.isOwnProfile && user.currentUser?.id === 1 && !currentData.deleted && (
+
+
Admin Actions
+
+
+ {currentData.suspended ? "Unsuspend Account" : "Suspend Account"}
+
+
+ Delete Account
+
+ {
+ setCurrentData({ ...currentData, verified });
+ }}
+ />
+
+
+ )}
- {currentData.bio !== undefined && (
+ {/* Verify button for non-admin owner */}
+ {!currentData.isOwnProfile && currentData.userId && user.currentUser?.id !== 1 && (
+
+ {
+ setCurrentData({ ...currentData, verified });
+ }}
+ />
+
+ )}
+
+ {/* Hide profile sections for deleted users */}
+ {!currentData.deleted && (
+
- )}
+ placeholder="username" />
- {currentData.memberSince && (
-
- )}
-
-
+ {currentData.bio !== undefined && (
+
+ )}
+
+ {currentData.memberSince && (
+
+ )}
+
+ {currentData.verified && (
+
+ )}
+
+ )}
{currentData.isOwnProfile && (
-
-
,
- document.getElementById("root")!
+
);
}
diff --git a/frontend/src/pages/chat/ui/SuspensionDialog.tsx b/frontend/src/pages/chat/ui/SuspensionDialog.tsx
new file mode 100644
index 0000000..b9d1e22
--- /dev/null
+++ b/frontend/src/pages/chat/ui/SuspensionDialog.tsx
@@ -0,0 +1,42 @@
+import { StyledDialog } from "@/core/components/StyledDialog";
+
+interface SuspensionDialogProps {
+ reason: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export function SuspensionDialog({ reason, open, onOpenChange }: SuspensionDialogProps) {
+ return (
+
+
+
+
+
+
+
+
Аккаунт заблокирован
+
+ Ваш аккаунт был заблокирован за нарушение правил сообщества.
+ Вы не можете отправлять сообщения или взаимодействовать с другими пользователями.
+
+ {reason && reason !== "No reason provided" && (
+
+
Причина блокировки:
+
+ {reason}
+
+
+ )}
+
+ Если вы считаете, что блокировка была применена по ошибке,
+ обратитесь к администратору для рассмотрения вашего случая.
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx
index f076f57..58dea0b 100644
--- a/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx
+++ b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx
@@ -4,6 +4,7 @@ import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
import { API_BASE_URL } from "@/core/config";
import { getAuthHeaders } from "@/core/api/authApi";
import { fetchUserPublicKey } from "@/core/api/dmApi";
+import { StatusBadge } from "@/core/components/StatusBadge";
import type { Message } from "@/core/types";
import { websocket } from "@/core/websocket";
import { onlineStatusManager } from "@/core/onlineStatusManager";
@@ -19,6 +20,7 @@ interface PublicChat {
interface DMConversation {
id: number;
+ userId: number;
username: string;
display_name: string;
profile_picture?: string;
@@ -27,6 +29,7 @@ interface DMConversation {
lastMessage?: string;
unreadCount: number;
publicKey?: string | null;
+ verified?: boolean;
}
type ChatItem = PublicChat | DMConversation;
@@ -84,6 +87,7 @@ export function UnifiedChatsList() {
const dmChatItems: ChatItem[] = dmUsers.map((user: DMUser) => ({
id: user.id,
+ userId: user.id, // Add userId field
username: user.username,
display_name: user.display_name,
profile_picture: user.profile_picture,
@@ -261,6 +265,14 @@ export function UnifiedChatsList() {
onClick={() => handleDMClick(chat)}
style={{ cursor: "pointer" }}
>
+
+ {chat.display_name}
+
+
{chat.lastMessage || "Нет сообщений"}
diff --git a/frontend/src/pages/chat/ui/left/UsernameSearch.tsx b/frontend/src/pages/chat/ui/left/UsernameSearch.tsx
index 800c194..f91cfcf 100644
--- a/frontend/src/pages/chat/ui/left/UsernameSearch.tsx
+++ b/frontend/src/pages/chat/ui/left/UsernameSearch.tsx
@@ -1,14 +1,16 @@
import { useState, useEffect } from "react";
import { useAppState } from "@/pages/chat/state";
import { searchUsers, fetchUserPublicKey } from "@/core/api/dmApi";
+import { StatusBadge } from "@/core/components/StatusBadge";
import type { User } from "@/core/types";
import { onlineStatusManager } from "@/core/onlineStatusManager";
-import { OnlineIndicator } from "../right/OnlineIndicator";
+import { OnlineIndicator } from "@/pages/chat/ui/right/OnlineIndicator";
import defaultAvatar from "@/images/default-avatar.png";
import SearchBar from "@/core/components/SearchBar";
interface SearchUser extends User {
publicKey?: string | null;
+ verified?: boolean;
}
export function UsernameSearch() {
@@ -68,10 +70,12 @@ export function UsernameSearch() {
};
}, [searchResults]);
+
async function handleUserClick(searchUser: SearchUser) {
if (!user.authToken) return;
try {
+
let publicKey = searchUser.publicKey;
if (!publicKey) {
const fetchedPublicKey = await fetchUserPublicKey(searchUser.id, user.authToken);
@@ -150,6 +154,14 @@ export function UsernameSearch() {
onClick={() => handleUserClick(searchUser)}
style={{ cursor: "pointer" }}
>
+
+ {searchUser.username}
+
+

{
if (isDm && message.files) {
message.files.forEach(async (file) => {
- console.log(file);
const isImage = /\.(png|jpg|jpeg|gif|webp)$/i.test(file.name || "");
if (isImage && file.encrypted && !decryptedFiles.has(file.path)) {
- console.log("Decrypting...");
const decryptedUrl = await decryptFile(file);
- console.log(decryptedUrl);
if (decryptedUrl) {
updateDecryptedFiles(draft => {
draft.set(file.path, decryptedUrl);
@@ -486,7 +484,7 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
{!isAuthor && !isDm && (

{
const target = e.target as HTMLImageElement;
@@ -502,6 +500,11 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
className="message-username"
onClick={handleProfileClick}>
{message.username}
+
)}
diff --git a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx
index 3ac3022..a999063 100644
--- a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx
+++ b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect, useRef } from "react";
import type { Message, Size2D } from "@/core/types";
import { EmojiMenu } from "./EmojiMenu";
+import { useAppState } from "@/pages/chat/state";
interface MessageContextMenuProps {
message: Message;
@@ -33,6 +34,7 @@ export function MessageContextMenu({
isOpen,
onOpenChange
}: MessageContextMenuProps) {
+ const { user } = useAppState();
// Internal state for closing animation
const [isClosing, setIsClosing] = useState(false);
const [calculatedPosition, setCalculatedPosition] = useState(position);
@@ -209,7 +211,7 @@ export function MessageContextMenu({
onDelete(message);
handleClose();
},
- show: isAuthor
+ show: isAuthor || user.currentUser?.id === 1
},
];
diff --git a/package.json b/package.json
index 0949b79..f4b9d31 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
"escape-string-regexp": "^5.0.0",
"marked": "^16.3.0",
"mdui": "^2.1.4",
+ "motion": "^12.23.24",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.3",