Fix user profile dialog

This commit is contained in:
2025-09-03 22:47:03 +03:00
Unverified
parent 7796e9577e
commit 182fe88db4
6 changed files with 169 additions and 48 deletions
@@ -2,14 +2,34 @@ import { useChat } from "../../hooks/useChat";
import { Message } from "./Message";
import { useAppState } from "../../state";
import type { Message as MessageType } from "../../../core/types";
import type { UserProfile } from "../../../core/types";
import { UserProfileDialog } from "./UserProfileDialog";
import { fetchUserProfile } from "../../api/profileApi";
import { useState } from "react";
import { delay } from "../../../utils/utils";
export function ChatMessages() {
const { messages } = useChat();
const { user } = useAppState();
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
const handleProfileClick = (username: string) => {
// TODO: Show user profile dialog
console.log("Show profile for:", username);
const handleProfileClick = async (username: string) => {
if (!user.authToken) return;
setIsLoadingProfile(true);
try {
const profile = await fetchUserProfile(user.authToken, username);
if (profile) {
setSelectedUserProfile(profile);
setProfileDialogOpen(true);
}
} catch (error) {
console.error("Failed to fetch user profile:", error);
} finally {
setIsLoadingProfile(false);
}
};
const handleContextMenu = (e: React.MouseEvent, message: MessageType) => {
@@ -19,16 +39,31 @@ export function ChatMessages() {
};
return (
<div className="chat-messages" id="chat-messages">
{messages.map((message) => (
<Message
key={message.id}
message={message}
isAuthor={message.username === user.currentUser?.username}
onProfileClick={handleProfileClick}
onContextMenu={handleContextMenu}
/>
))}
</div>
<>
<div className="chat-messages" id="chat-messages">
{messages.map((message) => (
<Message
key={message.id}
message={message}
isAuthor={message.username === user.currentUser?.username}
onProfileClick={handleProfileClick}
onContextMenu={handleContextMenu}
isLoadingProfile={isLoadingProfile}
/>
))}
</div>
<UserProfileDialog
isOpen={profileDialogOpen}
onOpenChange={async (value) => {
setProfileDialogOpen(value);
if (!value) {
await delay(1000);
setSelectedUserProfile(null);
}
}}
userProfile={selectedUserProfile}
/>
</>
);
}
+8 -6
View File
@@ -7,9 +7,10 @@ interface MessageProps {
isAuthor: boolean;
onProfileClick: (username: string) => void;
onContextMenu: (e: React.MouseEvent, message: MessageType) => void;
isLoadingProfile?: boolean;
}
export function Message({ message, isAuthor, onProfileClick, onContextMenu }: MessageProps) {
export function Message({ message, isAuthor, onProfileClick, onContextMenu, isLoadingProfile = false }: MessageProps) {
return (
<div
className={`message ${isAuthor ? "sent" : "received"}`}
@@ -23,8 +24,9 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu }: Me
<img
src={message.profile_picture || defaultAvatar}
alt={message.username}
onClick={() => onProfileClick(message.username)}
style={{ cursor: "pointer" }}
onClick={() => !isLoadingProfile && onProfileClick(message.username)}
style={{ cursor: isLoadingProfile ? "default" : "pointer" }}
className={isLoadingProfile ? "loading" : ""}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = defaultAvatar;
@@ -35,9 +37,9 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu }: Me
{!isAuthor && (
<div
className="message-username"
onClick={() => onProfileClick(message.username)}
style={{ cursor: "pointer" }}> {/* TODO extract to SCSS */}
className={`message-username ${isLoadingProfile ? "loading" : ""}`}
onClick={() => !isLoadingProfile && onProfileClick(message.username)}
style={{ cursor: isLoadingProfile ? "default" : "pointer" }}> {/* TODO extract to SCSS */}
{message.username}
</div>
)}
@@ -1,40 +1,71 @@
import type { DialogProps } from "../../../core/types";
import type { UserProfile } from "../../../core/types";
import { MaterialDialog } from "../Dialog";
import { formatTime } from "../../../utils/utils";
import defaultAvatar from "../../../resources/images/default-avatar.png";
export function UserProfileDialog({ isOpen, onOpenChange }: DialogProps) {
return (
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc>
<div className="content">
<div className="profile-picture-section">
<img className="profile-picture" alt="Profile Picture" />
interface UserProfileDialogProps extends DialogProps {
userProfile: UserProfile | null;
}
export function UserProfileDialog({ isOpen, onOpenChange, userProfile }: UserProfileDialogProps) {
const content = userProfile ? (
<div className="content">
<div className="profile-picture-section">
<img
className="profile-picture"
alt="Profile Picture"
src={userProfile.profile_picture || defaultAvatar}
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = defaultAvatar;
}}
/>
</div>
<div className="profile-info">
<div className="username-section">
<h4 className="username">{userProfile.username}</h4>
<div className={`online-status ${userProfile.online ? "online" : "offline"}`}>
{userProfile.online ? (
<>
<span className="online-indicator"></span> Online
</>
) : (
<>
<span className="offline-indicator"></span> Last seen {formatTime(userProfile.last_seen)}
</>
)}
</div>
</div>
<div className="profile-info">
<div className="username-section">
<h4 className="username"></h4>
<div className="online-status"></div>
<div className="bio-section">
<label>Bio:</label>
<div className="bio-display">
{userProfile.bio || "No bio available."}
</div>
<div className="bio-section">
<label>Bio:</label>
<div className="bio-display"></div>
</div>
<div className="profile-stats">
<div className="stat">
<span className="stat-label">Member since:</span>
<span className="stat-value member-since">{formatTime(userProfile.created_at)}</span>
</div>
<div className="profile-stats">
<div className="stat">
<span className="stat-label">Member since:</span>
<span className="stat-value member-since"></span>
</div>
<div className="stat">
<span className="stat-label">Last seen:</span>
<span className="stat-value last-seen"></span>
</div>
</div>
<div className="profile-actions">
<mdui-button id="dm-button" variant="filled">
<mdui-icon slot="icon" name="chat--filled"></mdui-icon>
Send Message
</mdui-button>
<div className="stat">
<span className="stat-label">Last seen:</span>
<span className="stat-value last-seen">{formatTime(userProfile.last_seen)}</span>
</div>
</div>
<div className="profile-actions">
<mdui-button id="dm-button" variant="filled">
<mdui-icon slot="icon" name="chat--filled"></mdui-icon>
Send Message
</mdui-button>
</div>
</div>
</div>
) : null
return (
<MaterialDialog open={isOpen} onOpenChange={onOpenChange} close-on-overlay-click close-on-esc id="user-profile-dialog">
{content}
</MaterialDialog>
);
}