feat: GIF search (Klipy), stickers, emoji picker, and bug fixes

- Add GIF search powered by Klipy API with correct response mapping
  (file.sm/hd tiers, not flat files structure)
- Add sticker system: packs, upload with auto-downscale, send in messages
- Add tabbed InputPopover with emoji, GIF, and sticker pickers
- Fix GIF API key migration race condition (column-add loop vs rename)
- Fix masked API key corruption on settings save (server + client guards)
- Fix sticker packs 403 (reversed isMember parameter order)
- Fix emoji picker not filling popover width (perLine 8→9, CSS 100%)
- Add error logging for Klipy API failures
This commit is contained in:
Jannis Braun
2026-03-15 02:04:37 +01:00
parent 7113f47b17
commit 3de6e4a668
26 changed files with 2160 additions and 50 deletions
@@ -11,21 +11,44 @@ export function GeneralPanel() {
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState('');
const [saveSuccess, setSaveSuccess] = useState(false);
const [gifKeyDirty, setGifKeyDirty] = useState(false);
const [gifKeyDraft, setGifKeyDraft] = useState('');
useEffect(() => {
if (instanceSettings) setDraft({ ...instanceSettings });
if (instanceSettings) {
setDraft({ ...instanceSettings });
// Don't populate the input with the masked value — show empty field
setGifKeyDraft('');
setGifKeyDirty(false);
}
}, [instanceSettings]);
if (!draft) return <div className="text-sm text-txt-tertiary">Loading settings...</div>;
const hasChanges = JSON.stringify(draft) !== JSON.stringify(instanceSettings);
const baseChanges = instanceSettings && draft
? draft.instanceName !== instanceSettings.instanceName ||
draft.registrationOpen !== instanceSettings.registrationOpen ||
draft.discoveryEnabled !== instanceSettings.discoveryEnabled
: false;
const hasChanges = baseChanges || gifKeyDirty;
const handleSave = async () => {
setSaving(true);
setSaveError('');
setSaveSuccess(false);
try {
await updateInstanceSettings(draft);
const payload: Partial<InstanceAdminSettings> = {
instanceName: draft!.instanceName,
registrationOpen: draft!.registrationOpen,
discoveryEnabled: draft!.discoveryEnabled,
};
// Only include gifApiKey when the user actually modified it
if (gifKeyDirty) {
payload.gifApiKey = gifKeyDraft;
}
await updateInstanceSettings(payload);
setGifKeyDirty(false);
setGifKeyDraft('');
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 2000);
} catch (err) {
@@ -37,6 +60,8 @@ export function GeneralPanel() {
const handleReset = () => {
if (instanceSettings) setDraft({ ...instanceSettings });
setGifKeyDirty(false);
setGifKeyDraft('');
setSaveError('');
};
@@ -90,6 +115,39 @@ export function GeneralPanel() {
</div>
</div>
{/* GIF Search */}
<div>
<div className="text-[11px] font-semibold text-txt-tertiary uppercase tracking-wider mb-1.5">GIF Search</div>
<p className="text-xs text-txt-tertiary mb-2">
Enable GIF search powered by Klipy. Get a free API key from the Klipy developer portal.
</p>
<div className="rounded-lg bg-white/[0.02] p-3.5 space-y-2">
<input
type="password"
value={gifKeyDirty ? gifKeyDraft : ''}
onChange={(e) => { setGifKeyDraft(e.target.value); setGifKeyDirty(true); }}
placeholder={draft.gifEnabled ? 'Key saved — enter new key to replace' : 'Klipy API key'}
className="input-standard w-full"
autoComplete="off"
/>
<div className="flex items-center gap-2">
<span className={`inline-flex items-center gap-1 text-[11px] font-medium px-1.5 py-0.5 rounded ${
draft.gifEnabled ? 'bg-status-online/15 text-status-online' : 'bg-white/5 text-txt-tertiary'
}`}>
{draft.gifEnabled ? 'Enabled' : 'Not configured'}
</span>
{draft.gifEnabled && !gifKeyDirty && (
<button
onClick={() => { setGifKeyDraft(''); setGifKeyDirty(true); }}
className="text-[11px] text-txt-tertiary hover:text-txt-danger transition-colors"
>
Clear key
</button>
)}
</div>
</div>
</div>
{/* Status messages */}
{saveError && (
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>