updated bots,channels and supergroups lists and edit screens

This commit is contained in:
onysd 2026-08-05 21:49:30 +03:00
parent 40d49d5e97
commit 0b3c861057
21 changed files with 796 additions and 198 deletions

View file

@ -34,18 +34,29 @@ function avatarInitials(firstName: string, lastName: string, username: string):
return out.toUpperCase();
}
type AvatarKind = "user" | "channel";
// Avatar renders a user's or channel's current photo, reading through the
// admin console's proxy (/api/accounts/{id}/avatar or /api/channels/{id}/avatar).
// It falls back to a gradient-tinted initials tile — identical to the public
// preview cards — when there's no photo or the fetch fails.
export function Avatar({
userID,
firstName,
lastName,
id,
kind = "user",
firstName = "",
lastName = "",
username = "",
title = "",
size = 34,
refreshKey
}: {
userID: number;
firstName: string;
lastName: string;
id: number;
kind?: AvatarKind;
firstName?: string;
lastName?: string;
username?: string;
// title is the channel/supergroup name, used for initials when kind="channel".
title?: string;
size?: number;
// refreshKey busts the browser's cached image (Cache-Control: max-age=300)
// right after an admin-driven avatar change, so the new photo shows up
@ -56,24 +67,25 @@ export function Avatar({
useEffect(() => {
setFailed(false);
}, [userID, refreshKey]);
}, [id, kind, refreshKey]);
if (failed) {
const [from, to] = avatarGradient(userID);
const [from, to] = avatarGradient(id);
return (
<div
className="avatar-fallback"
style={{ width: size, height: size, background: `linear-gradient(135deg, ${from}, ${to})`, fontSize: Math.round(size * 0.42) }}
>
{avatarInitials(firstName, lastName, username)}
{kind === "channel" ? avatarInitials(title, "", username) : avatarInitials(firstName, lastName, username)}
</div>
);
}
const basePath = kind === "channel" ? `/api/channels/${id}/avatar` : `/api/accounts/${id}/avatar`;
return (
<img
className="avatar-photo-img"
src={`/api/accounts/${userID}/avatar${refreshKey !== undefined ? `?v=${encodeURIComponent(String(refreshKey))}` : ""}`}
src={`${basePath}${refreshKey !== undefined ? `?v=${encodeURIComponent(String(refreshKey))}` : ""}`}
alt=""
loading="lazy"
style={{ width: size, height: size }}

View file

@ -4,11 +4,13 @@ import { createPortal } from "react-dom";
import { api, errorMessage } from "../api";
import { Alert } from "./ui";
// AccountAvatarModal uploads a new profile photo for an account. It follows
// CreateStickerSetModal's shape (a single reason field + direct execute,
// rather than ActionButton's JSON dry-run/confirm flow) since the payload is
// a multipart file upload, not a plain JSON body.
export function AccountAvatarModal({ userID, onClose, onDone }: { userID: number; onClose: () => void; onDone: () => void }) {
type AvatarModalKind = "user" | "channel";
// AvatarModal uploads a new photo for an account or a channel/supergroup. It
// follows CreateStickerSetModal's shape (a single reason field + direct
// execute, rather than ActionButton's JSON dry-run/confirm flow) since the
// payload is a multipart file upload, not a plain JSON body.
export function AvatarModal({ kind, id, onClose, onDone }: { kind: AvatarModalKind; id: number; onClose: () => void; onDone: () => void }) {
const [file, setFile] = useState<File | null>(null);
const [previewURL, setPreviewURL] = useState("");
const [reason, setReason] = useState("");
@ -37,10 +39,11 @@ export function AccountAvatarModal({ userID, onClose, onDone }: { userID: number
setBusy(true);
setError("");
try {
const idField = kind === "channel" ? "channel_id" : "user_id";
const form = new FormData();
form.set("metadata", JSON.stringify({ command_id: "", reason: reason.trim(), confirm: true, user_id: userID }));
form.set("metadata", JSON.stringify({ command_id: "", reason: reason.trim(), confirm: true, [idField]: id }));
form.set("file", file, file.name);
const result = await api.setAccountAvatar(form);
const result = kind === "channel" ? await api.setChannelAvatar(form) : await api.setAccountAvatar(form);
if (result.error) {
setError(result.error);
return;
@ -54,12 +57,14 @@ export function AccountAvatarModal({ userID, onClose, onDone }: { userID: number
}
}
const noun = kind === "channel" ? "Channel" : "Account";
return createPortal(
<div className="modal-backdrop" role="presentation">
<section className="modal command-modal" role="dialog" aria-modal="true" aria-label={"Change avatar"}>
<div className="modal-head">
<div>
<div className="eyebrow">{"Account"}</div>
<div className="eyebrow">{noun}</div>
<h2>{"Change avatar"}</h2>
</div>
<button className="icon-btn" type="button" onClick={onClose} disabled={busy} aria-label={"Close"}><X size={15} /></button>