updated bots,channels and supergroups lists and edit screens
This commit is contained in:
parent
40d49d5e97
commit
0b3c861057
21 changed files with 796 additions and 198 deletions
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue