web: add avatar file upload in profile editors
All checks were successful
CI / test (push) Successful in 28s
All checks were successful
CI / test (push) Successful in 28s
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, getSavedMessagesChat, joinChat, leaveChat, pinChat, unarchiveChat, unpinChat } from "../api/chats";
|
||||
import { archiveChat, clearChat, createPrivateChat, deleteChat, getChats, getSavedMessagesChat, joinChat, leaveChat, pinChat, requestUploadUrl, unarchiveChat, unpinChat, uploadToPresignedUrl } from "../api/chats";
|
||||
import { globalSearch } from "../api/search";
|
||||
import type { DiscoverChat, Message, UserSearchItem } from "../chat/types";
|
||||
import { addContact, addContactByEmail, listContacts, removeContact, updateMyProfile } from "../api/users";
|
||||
import { useAuthStore } from "../store/authStore";
|
||||
import { useChatStore } from "../store/chatStore";
|
||||
import { useUiStore } from "../store/uiStore";
|
||||
import { NewChatPanel } from "./NewChatPanel";
|
||||
import { SettingsPanel } from "./SettingsPanel";
|
||||
import { applyAppearancePreferences, getAppPreferences } from "../utils/preferences";
|
||||
@@ -18,6 +19,7 @@ export function ChatList() {
|
||||
const loadChats = useChatStore((s) => s.loadChats);
|
||||
const me = useAuthStore((s) => s.me);
|
||||
const logout = useAuthStore((s) => s.logout);
|
||||
const showToast = useUiStore((s) => s.showToast);
|
||||
const [search, setSearch] = useState("");
|
||||
const [userResults, setUserResults] = useState<UserSearchItem[]>([]);
|
||||
const [discoverResults, setDiscoverResults] = useState<DiscoverChat[]>([]);
|
||||
@@ -46,6 +48,7 @@ export function ChatList() {
|
||||
const [profileAllowPrivateMessages, setProfileAllowPrivateMessages] = useState(true);
|
||||
const [profileError, setProfileError] = useState<string | null>(null);
|
||||
const [profileSaving, setProfileSaving] = useState(false);
|
||||
const [profileAvatarUploading, setProfileAvatarUploading] = useState(false);
|
||||
const sidebarRef = useRef<HTMLElement | null>(null);
|
||||
const burgerMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const burgerButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
@@ -205,6 +208,22 @@ export function ChatList() {
|
||||
setProfileAllowPrivateMessages(me.allow_private_messages ?? true);
|
||||
}, [me]);
|
||||
|
||||
async function uploadProfileAvatar(file: File) {
|
||||
setProfileAvatarUploading(true);
|
||||
setProfileError(null);
|
||||
try {
|
||||
const upload = await requestUploadUrl(file);
|
||||
await uploadToPresignedUrl(upload.upload_url, upload.required_headers, file);
|
||||
setProfileAvatarUrl(upload.file_url);
|
||||
showToast("Avatar uploaded");
|
||||
} catch {
|
||||
setProfileError("Failed to upload avatar");
|
||||
showToast("Avatar upload failed");
|
||||
} finally {
|
||||
setProfileAvatarUploading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function openSavedMessages() {
|
||||
const saved = await getSavedMessagesChat();
|
||||
const updatedChats = await getChats();
|
||||
@@ -754,6 +773,37 @@ export function ChatList() {
|
||||
<div className="w-full rounded-xl border border-slate-700/80 bg-slate-900 p-3">
|
||||
<p className="mb-2 text-sm font-semibold">Edit profile</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3 rounded border border-slate-700/70 bg-slate-800/40 p-2">
|
||||
{profileAvatarUrl.trim() ? (
|
||||
<img alt="avatar preview" className="h-12 w-12 rounded-full object-cover" src={profileAvatarUrl.trim()} />
|
||||
) : (
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-slate-700 text-xs text-slate-300">No avatar</div>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<label className="cursor-pointer rounded bg-sky-500 px-3 py-1.5 text-xs font-semibold text-slate-950">
|
||||
{profileAvatarUploading ? "Uploading..." : "Upload avatar"}
|
||||
<input
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
disabled={profileAvatarUploading}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
e.currentTarget.value = "";
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
void uploadProfileAvatar(file);
|
||||
}}
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
{profileAvatarUrl.trim() ? (
|
||||
<button className="rounded bg-slate-700 px-3 py-1.5 text-xs" onClick={() => setProfileAvatarUrl("")} type="button">
|
||||
Remove
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<input className="w-full rounded bg-slate-800 px-3 py-2 text-sm" placeholder="Name" value={profileName} onChange={(e) => setProfileName(e.target.value)} />
|
||||
<input className="w-full rounded bg-slate-800 px-3 py-2 text-sm" placeholder="Username" value={profileUsername} onChange={(e) => setProfileUsername(e.target.value.replace("@", ""))} />
|
||||
<input className="w-full rounded bg-slate-800 px-3 py-2 text-sm" placeholder="Bio (optional)" value={profileBio} onChange={(e) => setProfileBio(e.target.value)} />
|
||||
@@ -771,7 +821,7 @@ export function ChatList() {
|
||||
<div className="mt-3 flex gap-2">
|
||||
<button
|
||||
className="flex-1 rounded bg-sky-500 px-3 py-2 text-sm font-semibold text-slate-950 disabled:opacity-60"
|
||||
disabled={profileSaving}
|
||||
disabled={profileSaving || profileAvatarUploading}
|
||||
onClick={async () => {
|
||||
setProfileSaving(true);
|
||||
setProfileError(null);
|
||||
|
||||
Reference in New Issue
Block a user