changed verifier bot to marksbot
This commit is contained in:
parent
6ade34970c
commit
fa5cfaf14d
16 changed files with 302 additions and 30 deletions
130
cmd/telesrv-admin/web/src/components/EmojiPicker.tsx
Normal file
130
cmd/telesrv-admin/web/src/components/EmojiPicker.tsx
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import { Check, Loader2, Search, X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { api, errorMessage } from "../api";
|
||||
import type { EmojiRow } from "../types";
|
||||
import { StaticLottie } from "./StaticLottie";
|
||||
|
||||
function isAnimated(mime: string): boolean {
|
||||
const m = mime.toLowerCase();
|
||||
return m.includes("tgsticker") || m.includes("lottie") || m.includes("json");
|
||||
}
|
||||
|
||||
function EmojiThumb({ row }: { row: EmojiRow }) {
|
||||
const [failed, setFailed] = useState(!isAnimated(row.MimeType));
|
||||
|
||||
useEffect(() => {
|
||||
setFailed(!isAnimated(row.MimeType));
|
||||
}, [row.DocumentID, row.MimeType]);
|
||||
|
||||
if (failed) {
|
||||
return <div className="emoji-picker-glyph">{row.Alt || "🙂"}</div>;
|
||||
}
|
||||
return (
|
||||
<StaticLottie
|
||||
className="emoji-picker-anim"
|
||||
cacheKey={row.DocumentID}
|
||||
loader={() => api.emojiAnimation(row.DocumentID)}
|
||||
onError={() => setFailed(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// EmojiPicker searches every custom-emoji document already on this
|
||||
// deployment -- unlike the Emoji admin page's per-pack browsing, this is a
|
||||
// flat document search with no system/non-system distinction, so a bundled
|
||||
// icon from a default pack (e.g. Topics' ✅) is just as findable as a
|
||||
// hand-uploaded one. Lets the caller select a document id by clicking it
|
||||
// instead of typing one from memory; used wherever a raw document id field
|
||||
// otherwise has nothing else in the panel to pick from (e.g. a
|
||||
// bot-verification icon).
|
||||
export function EmojiPicker({
|
||||
label,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (documentID: string) => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState("");
|
||||
const [rows, setRows] = useState<EmojiRow[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const selected = rows.find((row) => row.DocumentID === value) ?? null;
|
||||
|
||||
async function search() {
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const params = new URLSearchParams({ limit: "24" });
|
||||
if (query.trim()) params.set("q", query.trim());
|
||||
try {
|
||||
const result = await api.emoji(params);
|
||||
setRows(result.rows ?? []);
|
||||
} catch (err) {
|
||||
setError(errorMessage(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void search();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="entity-picker">
|
||||
<div className="picker-head">
|
||||
<span>{label}</span>
|
||||
{value ? (
|
||||
<button className="link-button" type="button" onClick={() => onChange("")}>
|
||||
<X size={13} /> {"Clear"}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{value ? (
|
||||
<div className="selected-entity">
|
||||
<Check size={15} />
|
||||
<div>
|
||||
<strong>{selected?.Alt || "—"}</strong>
|
||||
<span className="mono">{value}</span>
|
||||
</div>
|
||||
<span>{selected?.SetTitle || "-"}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="picker-search">
|
||||
<Search size={15} />
|
||||
<input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void search();
|
||||
}
|
||||
}}
|
||||
placeholder={"Search document ID or emoji"}
|
||||
/>
|
||||
<button className="btn compact-btn" type="button" onClick={search} disabled={busy}>
|
||||
{busy ? <Loader2 size={14} className="spin" /> : "Search"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <div className="picker-error">{error}</div>}
|
||||
<div className="picker-results emoji-picker-results">
|
||||
{rows.map((row) => (
|
||||
<button
|
||||
key={row.DocumentID}
|
||||
className={`picker-row emoji-picker-row ${value === row.DocumentID ? "selected" : ""}`}
|
||||
type="button"
|
||||
onClick={() => onChange(row.DocumentID)}
|
||||
>
|
||||
<EmojiThumb row={row} />
|
||||
<span className="mono">{row.DocumentID}</span>
|
||||
<span>{row.SetTitle || "—"}</span>
|
||||
</button>
|
||||
))}
|
||||
{rows.length === 0 && !busy ? <div className="picker-empty">{"No results"}</div> : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import {
|
|||
import { useEffect, useState, type ReactNode } from "react";
|
||||
import { api, APIError, errorMessage } from "../api";
|
||||
import { ActionButton } from "../components/ActionButton";
|
||||
import { EmojiPicker } from "../components/EmojiPicker";
|
||||
import { BotPicker } from "../components/EntityPicker";
|
||||
import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui";
|
||||
import { displayUsername, formatDate } from "../lib/format";
|
||||
|
|
@ -623,17 +624,9 @@ function IconsBlock({
|
|||
<>
|
||||
{canManage && (
|
||||
<section className="section-block">
|
||||
<SectionHead title={"Add or rename an icon"} text={"The document id has to name a real custom emoji document on this deployment; the Emoji section lists them with their ids. Adding an id that already exists renames it instead of duplicating it."} />
|
||||
<SectionHead title={"Add or rename an icon"} text={"Search and pick any custom-emoji document already on this deployment (including bundled/system ones). Adding an id that already exists renames it instead of duplicating it."} />
|
||||
<EmojiPicker label={"Document"} value={documentID} onChange={setDocumentID} />
|
||||
<div className="bot-create-fields">
|
||||
<label className="duration-field">
|
||||
<span>{"Document ID"}</span>
|
||||
<input
|
||||
value={documentID}
|
||||
onChange={(event) => setDocumentID(event.target.value)}
|
||||
inputMode="numeric"
|
||||
placeholder="5361371319611781774"
|
||||
/>
|
||||
</label>
|
||||
<label className="duration-field">
|
||||
<span>{"Name"}</span>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -11,9 +11,8 @@ import { StickerSetPreviewModal } from "./StickerSetPreviewModal";
|
|||
type StickerPageSize = 10 | 20 | 50 | 100 | "all";
|
||||
|
||||
// Shared list/manage view for one non-system sticker-set kind ("stickers" or
|
||||
// "emoji") — system packs (dice, animated emoji, premium/TON gifts, etc.) are
|
||||
// filtered out server-side and never reach this page; they aren't meant to be
|
||||
// hand-edited.
|
||||
// "emoji") — system packs (dice, animated emoji, gifts) aren't shown here,
|
||||
// they're not hand-edited.
|
||||
export function StickerSetsPage({ kind }: { kind: "stickers" | "emoji" }) {
|
||||
const [sets, setSets] = useState<StickerSetRow[]>([]);
|
||||
const [query, setQuery] = useState("");
|
||||
|
|
|
|||
|
|
@ -317,6 +317,26 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.emoji-picker-row {
|
||||
grid-template-columns: 36px minmax(140px, 1fr) minmax(100px, 1fr);
|
||||
}
|
||||
|
||||
.emoji-picker-glyph {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emoji-picker-anim {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.emoji-picker-anim canvas {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
}
|
||||
|
||||
.picker-error {
|
||||
color: var(--danger);
|
||||
background: var(--danger-tint);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue