Implement verification checkmark

This commit is contained in:
2025-10-21 22:49:26 +03:00
Unverified
parent 3293d91368
commit 9e19342998
23 changed files with 736 additions and 52 deletions
+51 -22
View File
@@ -6,8 +6,11 @@ import defaultAvatar from "@/images/default-avatar.png";
import { confirm } from "mdui/functions/confirm";
import { updateProfile, uploadProfilePicture, fetchUserProfileById } from "@/core/api/profileApi";
import { RichTextArea } from "@/core/components/RichTextArea";
import { StatusBadge } from "@/core/components/StatusBadge";
import { VerifyButton } from "@/core/components/VerifyButton";
import { onlineStatusManager } from "@/core/onlineStatusManager";
import { OnlineStatus } from "./right/OnlineStatus";
import { Input } from "@/core/components/Input";
interface SectionProps {
type: string;
@@ -17,7 +20,7 @@ interface SectionProps {
value?: string;
onChange?: (value: string) => void;
readOnly: boolean;
placeholder: string;
placeholder?: string;
textArea?: boolean;
}
@@ -33,23 +36,20 @@ function Section({ type, icon, label, error, value, onChange, readOnly, placehol
placeholder={placeholder}
className="value"
rows={1}
readOnly={readOnly}
/>
readOnly={readOnly} />
);
} else {
valueComponent = (
<input
className="value"
type="text"
value={value}
onChange={e => onChange(e.target.value)}
readOnly={readOnly} />
className="value"
type="text"
value={value}
onChange={e => onChange(e.target.value)}
readOnly={readOnly} />
);
}
} else {
valueComponent = (
<span className="value">{value}</span>
);
valueComponent = <span className="value">{value}</span>
}
return (
@@ -104,6 +104,7 @@ export function ProfileDialog() {
if (userProfile) {
freshData = {
...userProfile,
userId: userProfile.id, // Preserve the userId field
memberSince: userProfile.created_at,
isOwnProfile: profileData.isOwnProfile
};
@@ -162,6 +163,7 @@ export function ProfileDialog() {
}
}, [isOpen, currentData?.userId, currentData?.isOwnProfile]);
// Validate fields when data changes
useEffect(() => {
if (currentData && isOpen) {
@@ -373,6 +375,7 @@ export function ProfileDialog() {
});
}
const fabVisible = useMemo(() => {
let hasErrors = false;
Object.values(errors).forEach(error => {
@@ -413,14 +416,20 @@ export function ProfileDialog() {
</div>
<div className={`username-section ${errors.display_name ? 'error' : ''}`}>
<input
className="username-input"
type="text"
value={currentData.display_name}
onChange={handleDisplayNameChange}
readOnly={!currentData.isOwnProfile}
placeholder="Имя"
/>
<div className="username-with-badge">
<Input
autoresizing={true}
className="username-input"
type="text"
value={currentData.display_name}
onChange={handleDisplayNameChange}
readOnly={!currentData.isOwnProfile}
placeholder="Имя" />
<StatusBadge
verified={currentData.verified || false}
userId={currentData.userId}
size="large" />
</div>
{errors.display_name && (
<div className="error-message">{errors.display_name}</div>
)}
@@ -432,6 +441,19 @@ export function ProfileDialog() {
</div>
)}
{/* Verify button for owner */}
{!currentData.isOwnProfile && currentData.userId && (
<div className="verify-section">
<VerifyButton
userId={currentData.userId}
verified={currentData.verified || false}
onVerificationChange={(verified) => {
setCurrentData({ ...currentData, verified });
}}
/>
</div>
)}
<div className="profile-sections">
<Section
type="username"
@@ -452,8 +474,7 @@ export function ProfileDialog() {
onChange={handleBioChange}
readOnly={!currentData.isOwnProfile}
placeholder="Нет информации о себе"
textArea
/>
textArea />
)}
{currentData.memberSince && (
@@ -462,8 +483,16 @@ export function ProfileDialog() {
icon="calendar_month--filled"
label="Участник с:"
value={formatDate(currentData.memberSince)}
readOnly={true} />
)}
{currentData.verified && (
<Section
type="verified"
icon="verified--filled"
label="Верификация:"
value="Этот аккаунт - официальное лицо FromChat."
readOnly={true}
placeholder="Участник с:"
/>
)}
</div>
@@ -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" }}
>
<div slot="headline" className="dm-list-headline">
{chat.display_name}
<StatusBadge
verified={chat.verified || false}
userId={chat.userId}
size="small"
/>
</div>
<span slot="description" className="list-description">
{chat.lastMessage || "Нет сообщений"}
</span>
@@ -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" }}
>
<div slot="headline" className="search-result-headline">
{searchUser.username}
<StatusBadge
verified={searchUser.verified || false}
userId={searchUser.id}
size="small"
/>
</div>
<div slot="icon" style={{ position: "relative", width: "40px", height: "40px", display: "inline-block" }}>
<img
src={searchUser.profile_picture || defaultAvatar}
@@ -11,6 +11,7 @@ import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric";
import { getAuthHeaders } from "@/core/api/authApi";
import { useAppState } from "@/pages/chat/state";
import { fetchUserProfileById, fetchUserProfile } from "@/core/api/profileApi";
import { StatusBadge } from "@/core/components/StatusBadge";
import { ub64 } from "@/utils/utils";
import { useImmer } from "use-immer";
import { createPortal } from "react-dom";
@@ -502,6 +503,11 @@ export function Message({ message, isAuthor, onContextMenu, onReactionClick, isD
className="message-username"
onClick={handleProfileClick}>
{message.username}
<StatusBadge
verified={message.verified || false}
userId={message.user_id}
size="small"
/>
</div>
)}