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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user