[PR #766] feat: klipy gifbox url #768

Open
opened 2026-02-16 12:36:28 -05:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/stoatchat/for-web/pull/766
Author: @dorkian-gray
Created: 2/15/2026
Status: 🔄 Open

Base: mainHead: feat/klipy-gifbox-url


📝 Commits (10+)

  • c3d44bc feat(gifbox): read gifbox URL from API config instead of hardcoded api.gifbox.me
  • dfdf859 feat(gifbox): read gifbox URL and attribution from API config
  • 2c5b3e3 fix(gifbox): use defensive type casting for gifbox config access
  • 49bdae5 fix(gifbox): use defensive type casting for gifbox config access
  • 03d9288 fix(gifbox): fetch gifbox config directly from API root, bypassing SDK type stripping
  • 9320764 fix(gifbox): use VITE_API_URL env var for gifbox config fetch
  • 176d6ae fix(gifbox): send gif media URL instead of missing top-level url field
  • 5f2a647 fix(gifbox): fallback through gif format variants for send URL
  • 5fbc1af fix(gifbox): use webm format URL which is available from gifbox proxy
  • f8170cf fix(gifbox): use itemurl page link for GIF embeds instead of direct media URL

📊 Changes

1 file changed (+46 additions, -6 deletions)

View changed files

📝 packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx (+46 -6)

📄 Description

Summary

Makes the GIF picker's gifbox URL and search placeholder text configurable via the API root response, and sends GIF media URLs directly for proper embedding. Replaces the hardcoded api.gifbox.me URL with a dynamic configuration fetched at runtime.

Background

The GIF picker previously hardcoded https://api.gifbox.me in three places within GifPicker.tsx, forcing all instances - including self-hosted deployments - to use the official gifbox instance. Self-hosters running their own gifbox service (e.g. with Klipy or another GIF provider) had no way to point the client at it.

This PR depends on a corresponding backend change that exposes gifbox configuration in the API root response under features.gifbox, including url, enabled, and attribution fields.

Changes

packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx:

  • Added a module-level gifboxConfig signal that fetches the gifbox configuration directly from the API root endpoint (VITE_API_URL) on first use
  • Replaced all three hardcoded https://api.gifbox.me URLs with the configured gifboxConfig().url
  • Added enabled: !!gifboxConfig() to all useQuery calls so they wait for the gifbox URL to be available before firing
  • Made the search bar placeholder text configurable via gifboxConfig().attribution, falling back to "Search for GIFs..." when empty
  • Changed the GIF send action to use props.item.media_formats.gif.url (direct GIF media URL) instead of props.item.url (which is empty or a page URL depending on the provider)

Why fetch from the API root directly?

The client SDK (stoat.js) imports its RevoltConfig type from the stoat-api package, which does not yet include the gifbox field in its features type definition. At runtime, the SDK strips unknown fields from API responses when parsing them into typed objects - so client.configuration.features.gifbox is genuinely undefined, not just hidden from TypeScript.

Rather than forking or patching the SDK types, this PR fetches the gifbox config with a standalone fetch() call to the API root using import.meta.env.VITE_API_URL. The fetch is deduplicated at the module level so it only runs once regardless of how many times the GIF picker is opened.

Why send media_formats.gif.url instead of props.item.url?

The original Tenor flow sent a shareable page URL (e.g. tenor.com/view/...) as message content. January would fetch the page, extract OG metadata, and create a Special::GIF embed. Klipy's page URLs don't have OG metadata (client-side rendered SPA), so this approach doesn't work.

Instead, the client sends the direct .gif URL from media_formats.gif. The backend's gifbox proxy has been updated to request gif format alongside webm and tinywebm, and January has been updated to recognize static.klipy.com image URLs and create Special::GIF embeds for them, which the client renders inline without proxying.

How it works

  1. The backend reads gifbox from [hosts] and gifbox_attribution from the top level of Revolt.toml
  2. The API root endpoint (/) exposes these as features.gifbox.url and features.gifbox.attribution
  3. When the GIF picker mounts, it fetches the API root and stores the gifbox config in a shared Solid.js signal
  4. All GIF queries (categories, trending, search) use the configured URL instead of a hardcoded one
  5. When a user clicks a GIF, the client sends media_formats.gif.url as message content
  6. January recognizes the static.klipy.com URL and creates a Special::GIF embed
  7. The client renders the GIF inline, bypassing the January proxy (existing behaviour for Special::GIF embeds)

For instances that don't set a gifbox host, the backend reports enabled: false with an empty URL, and the GIF picker's queries remain disabled.

Related

Testing

Tested against a self-hosted Stoat instance with gifbox proxying to Klipy (https://api.klipy.com/v2), confirming that GIF categories, trending, search, and sending all work end-to-end. GIFs render inline as Special::GIF embeds. Attribution placeholder text confirmed working when gifbox_attribution is set.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/stoatchat/for-web/pull/766 **Author:** [@dorkian-gray](https://github.com/dorkian-gray) **Created:** 2/15/2026 **Status:** 🔄 Open **Base:** `main` ← **Head:** `feat/klipy-gifbox-url` --- ### 📝 Commits (10+) - [`c3d44bc`](https://github.com/stoatchat/for-web/commit/c3d44bcd95e79c6a75466e8af66311c312a6ed6f) feat(gifbox): read gifbox URL from API config instead of hardcoded api.gifbox.me - [`dfdf859`](https://github.com/stoatchat/for-web/commit/dfdf85969704f28877fe248738283244fd4edd53) feat(gifbox): read gifbox URL and attribution from API config - [`2c5b3e3`](https://github.com/stoatchat/for-web/commit/2c5b3e373c30ada3859f4b29fa07937c6b5463f9) fix(gifbox): use defensive type casting for gifbox config access - [`49bdae5`](https://github.com/stoatchat/for-web/commit/49bdae56e9ac335c2b8c7cccf614910422bb7edd) fix(gifbox): use defensive type casting for gifbox config access - [`03d9288`](https://github.com/stoatchat/for-web/commit/03d9288215366b8f94f96d59a89eb52f10f06843) fix(gifbox): fetch gifbox config directly from API root, bypassing SDK type stripping - [`9320764`](https://github.com/stoatchat/for-web/commit/932076431de1b046850a6d448780788a467da884) fix(gifbox): use VITE_API_URL env var for gifbox config fetch - [`176d6ae`](https://github.com/stoatchat/for-web/commit/176d6aeaf2a719e1a5d642d962a29147566090f4) fix(gifbox): send gif media URL instead of missing top-level url field - [`5f2a647`](https://github.com/stoatchat/for-web/commit/5f2a647b803024b4aa2be8a78df8ddd89471beb5) fix(gifbox): fallback through gif format variants for send URL - [`5fbc1af`](https://github.com/stoatchat/for-web/commit/5fbc1afb9969bcfec836ea5426d969674ab07f02) fix(gifbox): use webm format URL which is available from gifbox proxy - [`f8170cf`](https://github.com/stoatchat/for-web/commit/f8170cf67b7321828fba3633b90846cd889f137a) fix(gifbox): use itemurl page link for GIF embeds instead of direct media URL ### 📊 Changes **1 file changed** (+46 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx` (+46 -6) </details> ### 📄 Description ## Summary Makes the GIF picker's gifbox URL and search placeholder text configurable via the API root response, and sends GIF media URLs directly for proper embedding. Replaces the hardcoded `api.gifbox.me` URL with a dynamic configuration fetched at runtime. ## Background The GIF picker previously hardcoded `https://api.gifbox.me` in three places within `GifPicker.tsx`, forcing all instances - including self-hosted deployments - to use the official gifbox instance. Self-hosters running their own gifbox service (e.g. with Klipy or another GIF provider) had no way to point the client at it. This PR depends on a corresponding backend change that exposes gifbox configuration in the API root response under `features.gifbox`, including `url`, `enabled`, and `attribution` fields. ## Changes **`packages/client/components/ui/components/features/messaging/composition/picker/GifPicker.tsx`**: - Added a module-level `gifboxConfig` signal that fetches the gifbox configuration directly from the API root endpoint (`VITE_API_URL`) on first use - Replaced all three hardcoded `https://api.gifbox.me` URLs with the configured `gifboxConfig().url` - Added `enabled: !!gifboxConfig()` to all `useQuery` calls so they wait for the gifbox URL to be available before firing - Made the search bar placeholder text configurable via `gifboxConfig().attribution`, falling back to "Search for GIFs..." when empty - Changed the GIF send action to use `props.item.media_formats.gif.url` (direct GIF media URL) instead of `props.item.url` (which is empty or a page URL depending on the provider) ### Why fetch from the API root directly? The client SDK (`stoat.js`) imports its `RevoltConfig` type from the `stoat-api` package, which does not yet include the `gifbox` field in its `features` type definition. At runtime, the SDK strips unknown fields from API responses when parsing them into typed objects - so `client.configuration.features.gifbox` is genuinely `undefined`, not just hidden from TypeScript. Rather than forking or patching the SDK types, this PR fetches the gifbox config with a standalone `fetch()` call to the API root using `import.meta.env.VITE_API_URL`. The fetch is deduplicated at the module level so it only runs once regardless of how many times the GIF picker is opened. ### Why send `media_formats.gif.url` instead of `props.item.url`? The original Tenor flow sent a shareable page URL (e.g. `tenor.com/view/...`) as message content. January would fetch the page, extract OG metadata, and create a `Special::GIF` embed. Klipy's page URLs don't have OG metadata (client-side rendered SPA), so this approach doesn't work. Instead, the client sends the direct `.gif` URL from `media_formats.gif`. The backend's gifbox proxy has been updated to request `gif` format alongside `webm` and `tinywebm`, and January has been updated to recognize `static.klipy.com` image URLs and create `Special::GIF` embeds for them, which the client renders inline without proxying. ## How it works 1. The backend reads `gifbox` from `[hosts]` and `gifbox_attribution` from the top level of `Revolt.toml` 2. The API root endpoint (`/`) exposes these as `features.gifbox.url` and `features.gifbox.attribution` 3. When the GIF picker mounts, it fetches the API root and stores the gifbox config in a shared Solid.js signal 4. All GIF queries (`categories`, `trending`, `search`) use the configured URL instead of a hardcoded one 5. When a user clicks a GIF, the client sends `media_formats.gif.url` as message content 6. January recognizes the `static.klipy.com` URL and creates a `Special::GIF` embed 7. The client renders the GIF inline, bypassing the January proxy (existing behaviour for `Special::GIF` embeds) For instances that don't set a gifbox host, the backend reports `enabled: false` with an empty URL, and the GIF picker's queries remain disabled. ## Related - Backend PR: https://github.com/stoatchat/stoatchat/pull/579 ## Testing Tested against a self-hosted Stoat instance with gifbox proxying to Klipy (`https://api.klipy.com/v2`), confirming that GIF categories, trending, search, and sending all work end-to-end. GIFs render inline as `Special::GIF` embeds. Attribution placeholder text confirmed working when `gifbox_attribution` is set. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-02-16 12:36:28 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: stoatchat/for-web#768