mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement verification checkmark
This commit is contained in:
@@ -149,3 +149,44 @@ export async function fetchUserProfileById(token: string, userId: number): Promi
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles verification status for a user (owner only)
|
||||
*/
|
||||
export async function verifyUser(userId: number, token: string): Promise<{verified: boolean} | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/user/${userId}/verify`, {
|
||||
method: 'POST',
|
||||
headers: getAuthHeaders(token)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error verifying user:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user is similar to any verified user
|
||||
*/
|
||||
export async function checkUserSimilarity(userId: number, token: string): Promise<{isSimilar: boolean, similarTo?: string} | null> {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/user/check-similarity/${userId}`, {
|
||||
headers: getAuthHeaders(token)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error checking user similarity:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import useCombinedRefs from '@/core/hooks/useCombinedRefs';
|
||||
import { id } from '@/utils/utils';
|
||||
|
||||
interface AutoResizeInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
autoresizing?: true;
|
||||
placeholderMinWidth?: boolean;
|
||||
onAutosize?: (width: number) => void;
|
||||
}
|
||||
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
autoresizing?: false;
|
||||
placeholderMinWidth?: false;
|
||||
onAutosize?: undefined;
|
||||
}
|
||||
|
||||
export function Input({
|
||||
autoresizing = false,
|
||||
placeholderMinWidth = false,
|
||||
onAutosize,
|
||||
style: inputStyle,
|
||||
...inputProps
|
||||
}: AutoResizeInputProps | InputProps) {
|
||||
const [inputWidth, setInputWidth] = useState(0);
|
||||
|
||||
const sizerRef = useRef<HTMLDivElement>(null);
|
||||
const placeholderSizerRef = useRef<HTMLDivElement>(null);
|
||||
const [inputRef, inputElement] = useCombinedRefs<HTMLInputElement>();
|
||||
|
||||
const sizerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden',
|
||||
height: 0,
|
||||
overflow: 'scroll',
|
||||
whiteSpace: 'pre',
|
||||
};
|
||||
|
||||
const copyStyles = useCallback((styles: CSSStyleDeclaration, node: HTMLElement) => {
|
||||
node.style.fontSize = styles.fontSize;
|
||||
node.style.fontFamily = styles.fontFamily;
|
||||
node.style.fontWeight = styles.fontWeight;
|
||||
node.style.fontStyle = styles.fontStyle;
|
||||
node.style.letterSpacing = styles.letterSpacing;
|
||||
node.style.textTransform = styles.textTransform;
|
||||
}, []);
|
||||
|
||||
const updateInputWidth = useCallback(() => {
|
||||
if (!sizerRef.current || typeof sizerRef.current.scrollWidth === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newInputWidth: number;
|
||||
|
||||
if (inputProps.placeholder && (!inputProps.value || (inputProps.value && placeholderMinWidth))) {
|
||||
const sizerWidth = sizerRef.current.scrollWidth;
|
||||
const placeholderWidth = placeholderSizerRef.current?.scrollWidth || 0;
|
||||
newInputWidth = Math.max(sizerWidth, placeholderWidth) + 2;
|
||||
} else {
|
||||
newInputWidth = sizerRef.current.scrollWidth + 2;
|
||||
}
|
||||
|
||||
|
||||
if (newInputWidth !== inputWidth) {
|
||||
setInputWidth(newInputWidth);
|
||||
onAutosize?.(newInputWidth);
|
||||
}
|
||||
}, [inputProps.placeholder, inputProps.value, inputProps.type, placeholderMinWidth, inputWidth, onAutosize]);
|
||||
|
||||
const copyInputStyles = useCallback(() => {
|
||||
if (!inputElement.current || !window.getComputedStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inputStyles = window.getComputedStyle(inputElement.current);
|
||||
if (!inputStyles) {
|
||||
return;
|
||||
}
|
||||
|
||||
copyStyles(inputStyles, sizerRef.current!);
|
||||
if (placeholderSizerRef.current) {
|
||||
copyStyles(inputStyles, placeholderSizerRef.current);
|
||||
}
|
||||
}, [inputElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoresizing) {
|
||||
copyInputStyles();
|
||||
updateInputWidth();
|
||||
}
|
||||
}, [autoresizing, copyInputStyles, updateInputWidth]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoresizing) {
|
||||
updateInputWidth();
|
||||
}
|
||||
}, [inputProps.value, inputProps.placeholder, autoresizing, updateInputWidth]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
{...inputProps}
|
||||
ref={inputRef}
|
||||
style={{
|
||||
boxSizing: 'content-box',
|
||||
width: autoresizing ? `${inputWidth}px` : undefined,
|
||||
...inputStyle,
|
||||
}}
|
||||
/>
|
||||
{autoresizing && createPortal(
|
||||
<>
|
||||
<div ref={sizerRef} style={sizerStyle}>
|
||||
{inputProps.defaultValue || inputProps.value || ''}
|
||||
</div>
|
||||
{inputProps.placeholder && (
|
||||
<div ref={placeholderSizerRef} style={sizerStyle}>
|
||||
{inputProps.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</>,
|
||||
id("root")
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { TextField } from "mdui/components/text-field";
|
||||
|
||||
interface TextFieldProps extends React.ComponentPropsWithoutRef<"mdui-text-field"> {
|
||||
ref?: React.Ref<TextField>
|
||||
}
|
||||
|
||||
export function MaterialTextField({ ref, ...props }: TextFieldProps) {
|
||||
return <mdui-text-field autocomplete="off" ref={ref as React.Ref<HTMLElement>} {...props} />
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { checkUserSimilarity } from "@/core/api/profileApi";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
verified: boolean;
|
||||
userId?: number;
|
||||
size?: "small" | "medium" | "large";
|
||||
}
|
||||
|
||||
export function StatusBadge({ verified, userId, size = "small" }: StatusBadgeProps) {
|
||||
const [isSimilarToVerified, setIsSimilarToVerified] = useState(false);
|
||||
const { user } = useAppState();
|
||||
|
||||
const className = `status-badge ${size}`;
|
||||
|
||||
// Check similarity for unverified users
|
||||
useEffect(() => {
|
||||
if (!verified && userId && user.authToken) {
|
||||
checkUserSimilarity(userId, user.authToken)
|
||||
.then(result => {
|
||||
setIsSimilarToVerified(result?.isSimilar || false);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error checking similarity:', error);
|
||||
setIsSimilarToVerified(false);
|
||||
});
|
||||
} else {
|
||||
setIsSimilarToVerified(false);
|
||||
}
|
||||
}, [verified, userId, user.authToken]);
|
||||
|
||||
if (verified) {
|
||||
return (
|
||||
<span className={`${className} verified`} title="Подтверждённый аккаунт">
|
||||
<mdui-icon name="verified--filled" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSimilarToVerified) {
|
||||
return (
|
||||
<span className={`${className} warning`} title="Похож на подтверждённый аккаунт">
|
||||
<mdui-icon name="warning" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Don't show anything if not verified and not similar
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { TextField } from "mdui/components/text-field";
|
||||
|
||||
type TextFieldProps = React.ComponentPropsWithoutRef<"mdui-text-field">
|
||||
|
||||
export function MaterialTextField(props: TextFieldProps & { ref?: React.Ref<TextField> }) {
|
||||
return <mdui-text-field
|
||||
autocomplete="off"
|
||||
{...(props as TextFieldProps & { ref?: React.Ref<HTMLElement> })} />
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useState } from "react";
|
||||
import { verifyUser } from "@/core/api/profileApi";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
|
||||
interface VerifyButtonProps {
|
||||
userId: number;
|
||||
verified: boolean;
|
||||
onVerificationChange?: (verified: boolean) => void;
|
||||
}
|
||||
|
||||
export function VerifyButton({ userId, verified, onVerificationChange }: VerifyButtonProps) {
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
const { user } = useAppState();
|
||||
|
||||
// Only show for owner
|
||||
if (user.currentUser?.id !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleVerifyToggle() {
|
||||
if (!user.authToken || isVerifying) return;
|
||||
|
||||
setIsVerifying(true);
|
||||
try {
|
||||
const result = await verifyUser(userId, user.authToken);
|
||||
if (result) {
|
||||
onVerificationChange?.(result.verified);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error toggling verification:', error);
|
||||
} finally {
|
||||
setIsVerifying(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<mdui-button
|
||||
variant="filled"
|
||||
loading={isVerifying}
|
||||
onClick={handleVerifyToggle}
|
||||
title={verified ? "Снять подтверждение" : "Подтвердить аккаунт"}
|
||||
>
|
||||
{verified ? "Отменить подтверждение" : "Подтвердить"}
|
||||
</mdui-button>
|
||||
);
|
||||
}
|
||||
Vendored
+3
@@ -69,6 +69,7 @@ export interface Message {
|
||||
is_edited: boolean;
|
||||
timestamp: string;
|
||||
profile_picture?: string;
|
||||
verified?: boolean;
|
||||
reply_to?: Message;
|
||||
files?: Attachment[];
|
||||
reactions?: Reaction[];
|
||||
@@ -116,6 +117,7 @@ export interface User {
|
||||
admin?: boolean;
|
||||
bio?: string;
|
||||
profile_picture: string;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,6 +140,7 @@ export interface UserProfile {
|
||||
online: boolean;
|
||||
last_seen: string;
|
||||
created_at: string;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
// ----------
|
||||
|
||||
@@ -81,4 +81,102 @@ button, input {
|
||||
border-left: 3px solid $color-dark-primary;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Verified badge styles
|
||||
.verified-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: $color-dark-primary;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
|
||||
&.small {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Status badge styles (unified for verified and warning)
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
&.verified {
|
||||
color: $color-dark-primary;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #ff9800; // Orange color for warnings
|
||||
}
|
||||
|
||||
&.small mdui-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&.medium mdui-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&.large mdui-icon {
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// Profile dialog specific styles
|
||||
.username-with-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.similarity-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
background-color: $color-dark-error-container;
|
||||
color: $color-dark-on-error-container;
|
||||
border-radius: 8px;
|
||||
margin: 12px 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.verify-section {
|
||||
margin: 16px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-result-headline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dm-list-headline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { API_BASE_URL } from "@/core/config";
|
||||
import { useRef } from "react";
|
||||
import type { TextField } from "mdui/components/text-field";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import { MaterialTextField } from "@/core/components/TextField";
|
||||
import { MaterialTextField } from "@/core/components/MaterialTextField";
|
||||
import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications";
|
||||
import { isElectron } from "@/core/electron/electron";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TextField } from "mdui/components/text-field";
|
||||
import type { ErrorResponse, RegisterRequest, LoginResponse } from "@/core/types";
|
||||
import { API_BASE_URL } from "@/core/config";
|
||||
import { useAppState } from "@/pages/chat/state";
|
||||
import { MaterialTextField } from "@/core/components/TextField";
|
||||
import { MaterialTextField } from "@/core/components/MaterialTextField";
|
||||
import { ensureKeysOnLogin } from "@/core/api/authApi";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import "./auth.scss";
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
|
||||
@@ -99,18 +99,22 @@
|
||||
.username-section {
|
||||
text-align: center;
|
||||
|
||||
.username-input {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: $color-dark-on-surface;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: text;
|
||||
.username-with-badge {
|
||||
gap: 0;
|
||||
|
||||
.username-input {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: $color-dark-on-surface;
|
||||
text-align: center;
|
||||
outline: none;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +172,7 @@
|
||||
.label {
|
||||
font-size: small;
|
||||
color: $color-dark-on-surface-variant;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.value {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ProfileDialogData {
|
||||
memberSince?: string;
|
||||
online?: boolean;
|
||||
isOwnProfile: boolean;
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
interface ActiveDM {
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user