diff --git a/backend/models.py b/backend/models.py index c459c1b..bdc93a8 100644 --- a/backend/models.py +++ b/backend/models.py @@ -13,6 +13,7 @@ class User(Base): id = Column(Integer, primary_key=True, index=True) username = Column(String(50), unique=True, nullable=False, index=True) + display_name = Column(String(64), nullable=False) password_hash = Column(String(200), nullable=False) profile_picture = Column(String(255), nullable=True) bio = Column(Text, nullable=True) @@ -149,6 +150,7 @@ class LoginRequest(BaseModel): class RegisterRequest(BaseModel): username: str + display_name: str password: str confirm_password: str @@ -178,6 +180,7 @@ class PushSubscriptionRequest(BaseModel): class UserProfileResponse(BaseModel): id: int username: str + display_name: str profile_picture: str | None bio: str | None online: bool diff --git a/backend/routes/account.py b/backend/routes/account.py index 0448417..3b23b6f 100644 --- a/backend/routes/account.py +++ b/backend/routes/account.py @@ -6,7 +6,7 @@ from constants import OWNER_USERNAME from dependencies import get_current_user, get_db from models import LoginRequest, RegisterRequest, User, CryptoPublicKey, CryptoBackup from utils import create_token, get_password_hash, verify_password -from validation import is_valid_password, is_valid_username +from validation import is_valid_password, is_valid_username, is_valid_display_name router = APIRouter() @@ -17,6 +17,7 @@ def convert_user(user: User) -> dict: "last_seen": user.last_seen.isoformat(), "online": user.online, "username": user.username, + "display_name": user.display_name, "profile_picture": user.profile_picture, "bio": user.bio, "admin": user.username == OWNER_USERNAME @@ -58,6 +59,7 @@ def login(request: LoginRequest, db: Session = Depends(get_db)): @router.post("/register") def register(request: RegisterRequest, db: Session = Depends(get_db)): username = request.username.strip() + display_name = request.display_name.strip() password = request.password.strip() confirm_password = request.confirm_password.strip() @@ -75,7 +77,13 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)): if not is_valid_username(username): raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail="Имя пользователя должно быть от 3 до 20 символов и не содержать пробелов" + detail="Имя пользователя должно быть от 3 до 20 символов и содержать только английские буквы, цифры, дефисы и подчеркивания" + ) + + if not is_valid_display_name(display_name): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Отображаемое имя должно быть от 1 до 64 символов и не может быть пустым" ) if not is_valid_password(password): @@ -107,6 +115,7 @@ def register(request: RegisterRequest, db: Session = Depends(get_db)): hashed_password = get_password_hash(password) new_user = User( username=username, + display_name=display_name, password_hash=hashed_password, online=True, last_seen=datetime.now() diff --git a/backend/routes/messaging.py b/backend/routes/messaging.py index 6c1f2f2..54cc0e7 100644 --- a/backend/routes/messaging.py +++ b/backend/routes/messaging.py @@ -48,16 +48,17 @@ def convert_message(msg: Message) -> dict: reactions_dict[emoji]["count"] += 1 reactions_dict[emoji]["users"].append({ "id": reaction.user_id, - "username": reaction.user.username + "username": reaction.user.display_name }) return { "id": msg.id, + "user_id": msg.author.id, "content": msg.content, "timestamp": msg.timestamp.isoformat(), "is_read": msg.is_read, "is_edited": msg.is_edited, - "username": msg.author.username, + "username": msg.author.display_name, "profile_picture": msg.author.profile_picture, "reply_to": convert_message(msg.reply_to) if msg.reply_to else None, "reactions": list(reactions_dict.values()), @@ -88,7 +89,7 @@ def convert_dm_envelope(envelope: DMEnvelope) -> dict: reactions_dict[emoji]["count"] += 1 reactions_dict[emoji]["users"].append({ "id": reaction.user_id, - "username": reaction.user.username + "username": reaction.user.display_name }) return { diff --git a/backend/routes/profile.py b/backend/routes/profile.py index 8eb6563..6b0d800 100644 --- a/backend/routes/profile.py +++ b/backend/routes/profile.py @@ -11,12 +11,14 @@ import io from dependencies import get_db, get_current_user from models import User, UpdateBioRequest, UserProfileResponse from pydantic import BaseModel +from validation import is_valid_username, is_valid_display_name router = APIRouter() # Request models class UpdateProfileRequest(BaseModel): - nickname: str | None = None + username: str | None = None + display_name: str | None = None description: str | None = None # Create uploads directory if it doesn't exist @@ -102,6 +104,7 @@ async def get_user_profile( return { "id": current_user.id, "username": current_user.username, + "display_name": current_user.display_name, "profile_picture": current_user.profile_picture, "bio": current_user.bio, "online": current_user.online, @@ -121,19 +124,32 @@ async def update_user_profile( updated = False # Update username if provided - if request.nickname is not None: - nickname = request.nickname.strip() - if len(nickname) < 3: - raise HTTPException(status_code=400, detail="Username must be at least 3 characters long") - if len(nickname) > 50: - raise HTTPException(status_code=400, detail="Username must be 50 characters or less") + if request.username is not None: + username = request.username.strip() + if not is_valid_username(username): + raise HTTPException( + status_code=400, + detail="Имя пользователя должно быть от 3 до 20 символов и содержать только английские буквы, цифры, дефисы и подчеркивания" + ) # Check if username is already taken by another user - existing_user = db.query(User).filter(User.username == nickname, User.id != current_user.id).first() + existing_user = db.query(User).filter(User.username == username, User.id != current_user.id).first() if existing_user: - raise HTTPException(status_code=400, detail="Username already taken") + raise HTTPException(status_code=400, detail="Это имя пользователя уже занято") - current_user.username = nickname + current_user.username = username + updated = True + + # Update display name if provided + if request.display_name is not None: + display_name = request.display_name.strip() + if not is_valid_display_name(display_name): + raise HTTPException( + status_code=400, + detail="Отображаемое имя должно быть от 1 до 64 символов и не может быть пустым" + ) + + current_user.display_name = display_name updated = True # Update bio if provided @@ -150,12 +166,14 @@ async def update_user_profile( return { "message": "Profile updated successfully", "username": current_user.username, + "display_name": current_user.display_name, "bio": current_user.bio } else: return { "message": "No changes made", "username": current_user.username, + "display_name": current_user.display_name, "bio": current_user.bio } @@ -197,6 +215,31 @@ async def get_user_by_username( return UserProfileResponse( id=user.id, username=user.username, + display_name=user.display_name, + profile_picture=user.profile_picture, + bio=user.bio, + online=user.online, + last_seen=user.last_seen, + created_at=user.created_at + ) + +@router.get("/user/id/{user_id}") +async def get_user_by_id( + user_id: int, + db: Session = Depends(get_db) +): + """ + Get user profile by user ID + """ + user = db.query(User).filter(User.id == user_id).first() + + if not user: + raise HTTPException(status_code=404, detail="User not found") + + return UserProfileResponse( + id=user.id, + username=user.username, + display_name=user.display_name, profile_picture=user.profile_picture, bio=user.bio, online=user.online, diff --git a/backend/validation.py b/backend/validation.py index d145b6f..376fdc2 100644 --- a/backend/validation.py +++ b/backend/validation.py @@ -3,7 +3,17 @@ import re def is_valid_username(username: str) -> bool: if len(username) < 3 or len(username) > 20: return False - if re.search(r'[\s\u180E\u200B-\u200D\u2060\uFEFF]', username): + # Only allow English letters, numbers, dashes and underscores + if not re.match(r'^[a-zA-Z0-9_-]+$', username): + return False + return True + + +def is_valid_display_name(display_name: str) -> bool: + if len(display_name) < 1 or len(display_name) > 64: + return False + # Check if not blank (only whitespace) + if not display_name.strip(): return False return True diff --git a/frontend/src/core/api/profileApi.ts b/frontend/src/core/api/profileApi.ts index 53c0962..98ca83e 100644 --- a/frontend/src/core/api/profileApi.ts +++ b/frontend/src/core/api/profileApi.ts @@ -4,7 +4,8 @@ import type { UserProfile } from "@/core/types"; export interface ProfileData { profile_picture?: string; - nickname?: string; + username?: string; + display_name?: string; description?: string; } @@ -26,7 +27,8 @@ export async function loadProfile(token: string): Promise { // Map backend fields to frontend fields return { profile_picture: data.profile_picture, - nickname: data.username, + username: data.username, + display_name: data.display_name, description: data.bio }; } @@ -69,7 +71,8 @@ export async function updateProfile(token: string, data: Partial): try { // Map frontend fields to backend fields const backendData = { - nickname: data.nickname, + username: data.username, + display_name: data.display_name, description: data.description }; @@ -126,3 +129,23 @@ export async function fetchUserProfile(token: string, username: string): Promise return null; } } + +/** + * Fetches user profile data by user ID + */ +export async function fetchUserProfileById(token: string, userId: number): Promise { + try { + const response = await fetch(`${API_BASE_URL}/user/id/${userId}`, { + headers: getAuthHeaders(token) + }); + + if (response.ok) { + return await response.json(); + } + + return null; + } catch (error) { + console.error('Error fetching user profile by ID:', error); + return null; + } +} diff --git a/frontend/src/core/types.d.ts b/frontend/src/core/types.d.ts index 0c13a15..47fc9d6 100644 --- a/frontend/src/core/types.d.ts +++ b/frontend/src/core/types.d.ts @@ -62,6 +62,7 @@ export interface Reaction { export interface Message { id: number; + user_id: number; username: string; content: string; is_read: boolean; @@ -111,6 +112,7 @@ export interface User { last_seen: string; online: boolean; username: string; + display_name: string; admin?: boolean; bio?: string; profile_picture: string; @@ -130,6 +132,7 @@ export interface User { export interface UserProfile { id: number; username: string; + display_name: string; profile_picture?: string; bio?: string; online: boolean; @@ -163,6 +166,7 @@ export interface LoginRequest { */ export interface RegisterRequest { username: string; + display_name: string; password: string; confirm_password: string; } diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 0cec4c5..91400e9 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -105,7 +105,7 @@ export default function LoginPage() { } }}> { alerts.push({type: type, message: message}) }); } + const displayNameElement = useRef(null); const usernameElement = useRef(null); const passwordElement = useRef(null); const confirmPasswordElement = useRef(null); @@ -36,11 +37,12 @@ export default function RegisterPage() {
{ e.preventDefault(); + const displayName = displayNameElement.current!.value.trim(); const username = usernameElement.current!.value.trim(); const password = passwordElement.current!.value.trim(); const confirmPassword = confirmPasswordElement.current!.value.trim(); - if (!username || !password || !confirmPassword) { + if (!displayName || !username || !password || !confirmPassword) { showAlert("danger", "Пожалуйста, заполните все поля"); return; } @@ -50,11 +52,22 @@ export default function RegisterPage() { return; } + if (displayName.length < 1 || displayName.length > 64) { + showAlert("danger", "Отображаемое имя должно быть от 1 до 64 символов"); + return; + } + if (username.length < 3 || username.length > 20) { showAlert("danger", "Имя пользователя должно быть от 3 до 20 символов"); return; } + // Validate username format (only English letters, numbers, dashes, underscores) + if (!/^[a-zA-Z0-9_-]+$/.test(username)) { + showAlert("danger", "Имя пользователя может содержать только английские буквы, цифры, дефисы и подчеркивания"); + return; + } + if (password.length < 5 || password.length > 50) { showAlert("danger", "Пароль должен быть от 5 до 50 символов"); return; @@ -62,6 +75,7 @@ export default function RegisterPage() { try { const request: RegisterRequest = { + display_name: displayName, username: username, password: password, confirm_password: confirmPassword @@ -97,7 +111,18 @@ export default function RegisterPage() { } }}> + void; + readOnly: boolean; + placeholder: string; + textArea?: boolean; +} + +function Section({ type, icon, label, error, value, onChange, readOnly, placeholder, textArea = false }: SectionProps) { + let valueComponent: ReactNode = null; + + if (onChange) { + if (textArea) { + valueComponent = ( + + ); + } else { + valueComponent = ( + onChange(e.target.value)} + readOnly={readOnly} /> + ); + } + } else { + valueComponent = ( + {value} + ); + } + + return ( +
+ +
+ + {valueComponent} + {error && ( +
{error}
+ )} +
+
+ ) +} + export function ProfileDialog() { - const { chat, user, closeProfileDialog } = useAppState(); + const { chat, user, closeProfileDialog, setUser } = useAppState(); const [isOpen, setIsOpen] = useState(false); const [originalData, setOriginalData] = useState(null); const [currentData, setCurrentData] = useState(null); const [isSaving, setIsSaving] = useState(false); + const [errors, setErrors] = useState<{[key: string]: string}>({}); const fileInputRef = useRef(null); const backdropRef = useRef(null); const dialogRef = useRef(null); @@ -40,23 +98,19 @@ export function ProfileDialog() { } }, [chat.profileDialog, isOpen]); - const fetchFreshProfileData = async (profileData: ProfileDialogData) => { + async function fetchFreshProfileData(profileData: ProfileDialogData) { if (!user.authToken) return; try { let freshData = profileData; - // If it's not the public chat and has a username, fetch fresh data - if (profileData.username && profileData.username !== "Общий чат" && profileData.userId) { - const userProfile = await fetchUserProfile(user.authToken, profileData.username); + // If it's not the public chat and has a user ID, fetch fresh data + if (profileData.userId && profileData.username !== "Общий чат") { + const userProfile = await fetchUserProfileById(user.authToken, profileData.userId); if (userProfile) { freshData = { - userId: userProfile.id, - username: userProfile.username, - profilePicture: userProfile.profile_picture, - bio: userProfile.bio, + ...userProfile, memberSince: userProfile.created_at, - online: userProfile.online, isOwnProfile: profileData.isOwnProfile }; } @@ -72,7 +126,7 @@ export function ProfileDialog() { setCurrentData(profileData); setIsOpen(true); } - }; + } // Trigger transition after component mounts useEffect(() => { @@ -90,13 +144,13 @@ export function ProfileDialog() { // Handle ESC key useEffect(() => { - const handleEsc = (e: KeyboardEvent) => { - if (e.key === "Escape" && isOpen) { - handleClose(); - } - }; - if (isOpen) { + function handleEsc(e: KeyboardEvent) { + if (e.key === "Escape") { + handleClose(); + } + } + document.addEventListener("keydown", handleEsc); return () => document.removeEventListener("keydown", handleEsc); } @@ -117,6 +171,13 @@ export function ProfileDialog() { } }, [isOpen, currentData?.userId, currentData?.isOwnProfile]); + // Validate fields when data changes + useEffect(() => { + if (currentData && isOpen) { + validateFields(); + } + }, [currentData, isOpen]); + const hasChanges = useMemo(() => { if (!originalData || !currentData) return false; @@ -127,13 +188,14 @@ export function ProfileDialog() { }; return ( + normalizeValue(originalData.display_name) !== normalizeValue(currentData.display_name) || normalizeValue(originalData.username) !== normalizeValue(currentData.username) || normalizeValue(originalData.bio) !== normalizeValue(currentData.bio) || originalData.profilePicture !== currentData.profilePicture ); }, [originalData, currentData]); - const handleClose = async () => { + async function handleClose() { if (hasChanges) { try { await confirm({ @@ -151,7 +213,7 @@ export function ProfileDialog() { } }; - const triggerCloseAnimation = () => { + function triggerCloseAnimation() { if (backdropRef.current && dialogRef.current) { backdropRef.current.classList.remove('open'); dialogRef.current.classList.remove('open'); @@ -165,29 +227,41 @@ export function ProfileDialog() { } }; - const handleBackdropClick = (e: React.MouseEvent) => { + function handleBackdropClick(e: React.MouseEvent) { if (e.target === e.currentTarget) { handleClose(); } }; - const handleUsernameChange = (e: React.ChangeEvent) => { + function handleDisplayNameChange(e: React.ChangeEvent) { if (!currentData) return; - setCurrentData({ ...currentData, username: e.target.value }); + const newValue = e.target.value; + setCurrentData({ ...currentData, display_name: newValue }); + + // Validate display name in real-time + validateDisplayName(newValue); }; - const handleBioChange = (newBio: string) => { + function handleUsernameChange(value: string) { + if (!currentData) return; + setCurrentData({ ...currentData, username: value }); + + // Validate username in real-time + validateUsername(value); + }; + + function handleBioChange(newBio: string) { if (!currentData) return; setCurrentData({ ...currentData, bio: newBio }); }; - const handleProfilePictureClick = () => { + function handleProfilePictureClick() { if (currentData?.isOwnProfile) { fileInputRef.current?.click(); } }; - const handleFileSelect = (e: React.ChangeEvent) => { + function handleFileSelect(e: React.ChangeEvent) { const file = e.target.files?.[0]; if (file && file.type.startsWith("image/")) { // Open cropper dialog here - for now just update the image @@ -202,15 +276,60 @@ export function ProfileDialog() { } }; - const handleSave = async () => { + function validateDisplayName(value: string) { + let error = ""; + + if (!value || value.trim().length === 0) { + error = "Отображаемое имя не может быть пустым"; + } else if (value.length > 64) { + error = "Отображаемое имя не может быть длиннее 64 символов"; + } + + setErrors(prev => ({ ...prev, display_name: error })); + }; + + function validateUsername(value: string) { + let error = ""; + + if (!value || value.trim().length === 0) { + error = "Имя пользователя не может быть пустым"; + } else if (value.length < 3) { + error = "Имя пользователя должно быть не менее 3 символов"; + } else if (value.length > 20) { + error = "Имя пользователя не может быть длиннее 20 символов"; + } else if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + error = "Имя пользователя может содержать только английские буквы, цифры, дефисы и подчеркивания"; + } + + setErrors(prev => ({ ...prev, username: error })); + }; + + function validateFields() { + if (currentData) { + validateDisplayName(currentData.display_name || ""); + validateUsername(currentData.username || ""); + } + + return !errors.display_name && !errors.username; + }; + + async function handleSave() { if (!currentData || !user.authToken || !originalData) return; + // Validate fields first + if (!validateFields()) { + return; + } + setIsSaving(true); try { // Update profile data const updateData: any = {}; + if (originalData.display_name !== currentData.display_name) { + updateData.display_name = currentData.display_name; + } if (originalData.username !== currentData.username) { - updateData.nickname = currentData.username; + updateData.username = currentData.username; } if (originalData.bio !== currentData.bio) { updateData.description = currentData.bio; @@ -233,22 +352,51 @@ export function ProfileDialog() { // Update the original data to match current data setOriginalData(currentData); + // If this is the current user's profile and username was changed, update the current user data + if (currentData.isOwnProfile && user.currentUser && user.authToken) { + const updatedUser = { + ...user.currentUser, + username: currentData.username || user.currentUser.username, + display_name: currentData.display_name || user.currentUser.display_name, + bio: currentData.bio || user.currentUser.bio, + profile_picture: currentData.profilePicture || user.currentUser.profile_picture + }; + setUser(user.authToken, updatedUser); + } + // Close dialog with animation after successful save triggerCloseAnimation(); } catch (error) { console.error("Failed to save profile:", error); + // Handle API errors + if (error instanceof Error && error.message.includes("уже занято")) { + setErrors({ username: "Это имя пользователя уже занято" }); + } else { + setErrors({ general: "Ошибка при сохранении профиля" }); + } } finally { setIsSaving(false); } - }; + } - const formatDate = (dateString: string) => { + function formatDate(dateString: string) { return new Date(dateString).toLocaleDateString("ru-RU", { year: "numeric", month: "long", day: "numeric" }); - }; + } + + const fabVisible = useMemo(() => { + let hasErrors = false; + Object.values(errors).forEach(error => { + if (error) { + hasErrors = true; + } + }); + + return hasChanges && currentData?.isOwnProfile && !isSaving && !hasErrors; + }, [hasChanges, currentData?.isOwnProfile, isSaving, errors]); if (!isOpen || !currentData) return null; @@ -281,57 +429,64 @@ export function ProfileDialog() { )} - {/* Username */} - {currentData.username && ( -
- -
- )} + {/* Display Name */} +
+ + {errors.display_name && ( +
{errors.display_name}
+ )} +
{/* Online Status */} - {currentData?.userId && ( + {(currentData?.userId || currentData?.isOwnProfile) && (
- +
)}
+
+ + {/* Bio */} {currentData.bio !== undefined && ( -
- -
- - -
-
+
)} {/* Member Since */} {currentData.memberSince && ( -
- -
- Участник с: - - {formatDate(currentData.memberSince)} - -
-
+
)}
@@ -340,7 +495,7 @@ export function ProfileDialog() { {currentData.isOwnProfile && ( diff --git a/frontend/src/pages/chat/ui/left/ChatHeader.tsx b/frontend/src/pages/chat/ui/left/ChatHeader.tsx index fd112a5..ed69d72 100644 --- a/frontend/src/pages/chat/ui/left/ChatHeader.tsx +++ b/frontend/src/pages/chat/ui/left/ChatHeader.tsx @@ -13,7 +13,8 @@ export function ChatHeader() { const handleProfileClick = () => { setProfileDialog({ userId: user.currentUser?.id, - username: profileData?.nickname || "Пользователь", + username: profileData?.username || "Пользователь", + display_name: profileData?.display_name || "Пользователь", profilePicture: profileData?.profile_picture, bio: profileData?.description, memberSince: user.currentUser?.created_at, diff --git a/frontend/src/pages/chat/ui/left/ChatTabs.tsx b/frontend/src/pages/chat/ui/left/ChatTabs.tsx index 729cf55..5733364 100644 --- a/frontend/src/pages/chat/ui/left/ChatTabs.tsx +++ b/frontend/src/pages/chat/ui/left/ChatTabs.tsx @@ -1,21 +1,16 @@ import { useAppState, type ChatTabs } from "@/pages/chat/state"; import { UnifiedChatsList } from "./UnifiedChatsList"; -import type { FormEvent } from "react"; import type { Tabs } from "mdui/components/tabs"; export function ChatTabs() { const { chat, setActiveTab } = useAppState(); - function handleChange(e: FormEvent & CustomEvent<{ value: string }>) { - setActiveTab(e.detail.value as ChatTabs); - } - return (
+ onChange={(e) => setActiveTab((e.target as Tabs).value as ChatTabs)}> Чаты diff --git a/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx index 4c1bcfa..f076f57 100644 --- a/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx +++ b/frontend/src/pages/chat/ui/left/UnifiedChatsList.tsx @@ -7,7 +7,7 @@ import { fetchUserPublicKey } from "@/core/api/dmApi"; import type { Message } from "@/core/types"; import { websocket } from "@/core/websocket"; 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"; interface PublicChat { @@ -20,6 +20,7 @@ interface PublicChat { interface DMConversation { id: number; username: string; + display_name: string; profile_picture?: string; online?: boolean; type: "dm"; @@ -84,6 +85,7 @@ export function UnifiedChatsList() { const dmChatItems: ChatItem[] = dmUsers.map((user: DMUser) => ({ id: user.id, username: user.username, + display_name: user.display_name, profile_picture: user.profile_picture, online: user.online, type: "dm" as const, @@ -172,13 +174,13 @@ export function UnifiedChatsList() { }; }, [allChats]); - const formatPublicChatMessage = (chatId: string): string => { + function formatPublicChatMessage(chatId: string): string { const lastMessage = lastMessages[chatId]; if (!lastMessage) { return ""; } - const isCurrentUser = lastMessage.username === user.currentUser?.username; + const isCurrentUser = lastMessage.user_id === user.currentUser?.id; const prefix = isCurrentUser ? "Вы: " : `${lastMessage.username}: `; const maxContentLength = 50 - prefix.length; @@ -187,14 +189,13 @@ export function UnifiedChatsList() { : lastMessage.content; return prefix + content; - }; + } - - const handlePublicChatClick = async (chatName: string) => { + async function handlePublicChatClick(chatName: string) { await switchToPublicChat(chatName); - }; + } - const handleDMClick = async (dmConversation: DMConversation) => { + async function handleDMClick(dmConversation: DMConversation) { if (!dmConversation.publicKey) { const authToken = useAppState.getState().user.authToken; if (!authToken) return; @@ -215,7 +216,7 @@ export function UnifiedChatsList() { profilePicture: dmConversation.profile_picture, online: dmConversation.online || false }); - }; + } if (isLoadingUsers) { return ( @@ -256,7 +257,7 @@ export function UnifiedChatsList() { return ( handleDMClick(chat)} style={{ cursor: "pointer" }} > @@ -266,7 +267,7 @@ export function UnifiedChatsList() {
{chat.username} m.id === -1 && m.runtimeData?.sendingState?.tempId); @@ -199,7 +199,8 @@ export class PublicChatPanel extends MessagePanel { async getProfile(): Promise { return { - username: "Общий чат", + username: "general", + display_name: "Общий чат", bio: "Общаемся со всеми пользователями FromChat!", isOwnProfile: false };