Files
GO-3.0/HUB_GIF_PICKER.md
simon 7ecf19f1ae GO 3.0 v0.0.5: push field builds since v0.0.1, chat GIF picker, Hub-dev notes
Gitea was last updated at 0bb0c74a (v0.0.1). This commit is v0.0.2–v0.0.5:
QDN GIF library, in-app Q-Share updater, compact composer, and the chat/save
fixes that never left the working tree. README/CHANGELOG cover that gap;
HUB_GIF_PICKER.md is the note for Hub to copy the picker.
2026-08-14 19:18:19 +00:00

186 lines
5.4 KiB
Markdown

# Message to Hub developers — chat GIF picker
This is the note for **upstream Qortal Hub** (Electron / desktop). Please port
the GIF picker from this GO 3.0 fork so Hub and GO send the same chat payload
and can display each other's GIFs.
**Repo:** https://gitea.qortal.link/simon/GO-3.0 (`main`, GO 3.0 **v0.0.5**)
**Copy this folder as-is:** `src/components/Chat/gifs/`
Do **not** invent a new GIF protocol. The library already lives on QDN
(`qortal://APP/GIFs/`). Chat only stores a pointer.
---
## What a selected GIF is
QDN `IMAGE` resources whose identifier starts with `gif-`. Selecting one must
**attach a pointer**, not re-publish the file and not send the bytes over
Reticulum.
```ts
{
identifier, // starts with "gif-"
libraryGif: true,
mimeType: "image/gif",
name, // publisher QDN name
service: "IMAGE",
timestamp
}
```
Helper in this repo: `toChatImagePointer()` in `src/components/Chat/gifs/libraryGifs.ts`.
Embed (same as the GIFs Q-App copy button):
```
qortal://use-embed/IMAGE?name=NAME&identifier=ID&service=IMAGE&mimeType=image%2Fgif&timestamp=TS
```
---
## QDN endpoints (node already used by Hub)
Catalog — **one fetch**, then filter locally. Do not search QDN on every keystroke.
```
GET {node}/arbitrary/resources/search
?excludeblocked=true
&identifier=gif-
&includemetadata=true
&limit=100
&mode=ALL
&prefix=true
&reverse=true
&service=IMAGE
```
Moderation list:
```
GET {node}/arbitrary/DOCUMENT/GIFs/gifs_moderation
```
Bytes (queued + cached in `gifMediaCache.ts`; concurrency 2):
```
GET {node}/arbitrary/IMAGE/{name}/{identifier}
GET {node}/arbitrary/IMAGE/{name}/{identifier}?async=true // gather
```
`getBaseApiReact()` already points at the connected node. Hub already depends
on `react-intersection-observer` (used by `GifImage` for in-view lazy load).
---
## Files to copy
```
src/components/Chat/gifs/libraryGifs.ts
src/components/Chat/gifs/gifMediaCache.ts
src/components/Chat/gifs/gifUsage.ts
src/components/Chat/gifs/GifImage.tsx
src/components/Chat/gifs/ChatGifPicker.tsx
src/components/Chat/gifs/EmojiGifStage.tsx
src/components/Chat/gifs/EmojiGifSwitch.tsx
```
### One desktop tweak
`EmojiGifSwitch.tsx` imports `keepPanelKeyboardDown` from
`src/components/Mobile/emojiPanelBus.ts` (Android soft-keyboard). On Hub,
delete that import and the `keepPanelKeyboardDown()` call. Keep
`onMouseDown` / `onPointerDown` `preventDefault` so the composer does not
lose focus.
---
## Wire it (desktop path — `ReactionPicker`, not the mobile keyboard panel)
### 1. `src/components/ReactionPicker.tsx`
- Add optional `onGifSelect?: (gif: LibraryGif) => void`.
- When that callback is passed, wrap `emoji-picker-react` + `ChatGifPicker` in
`EmojiGifStage` and put `EmojiGifSwitch` under the stage.
- Match this fork's `ReactionPicker.tsx`.
### 2. `src/components/Chat/TipTap.tsx`
- Add `insertGif?: (gif: LibraryGif) => void`.
- On the **desktop** emoji button (the existing `ReactionPicker`), pass
`onGifSelect={insertGif}`.
- Do **not** port `MobileEmojiPanel`, `emojiPanelBus`, `useSoftKeyboardInset`,
or the Capacitor SoftKeyboard plugin. Those are Android-only.
### 3. `ChatGroup.tsx` and `ChatDirect.tsx`
- `pendingLibraryGif` state.
- `insertLibraryGif`: refuse if another image / attachment is already pending.
- On send, prefer the pointer over a file upload:
```ts
const images = pendingLibraryGif
? [toChatImagePointer(pendingLibraryGif)]
: existingImagePath;
```
- Composer preview: `<GifImage eager fill gif={pendingLibraryGif} />`.
### 4. `src/components/Chat/MessageItem.tsx`
- If `isLibraryGifImage(message.images[0])`, render
`<GifImage fit="contain" gif={…} />`.
- Skip the normal image-download / Reticulum-resource path for these.
### 5. `src/utils/chat.ts` — `buildImageEmbedLink`
Treat `libraryGif === true`, identifier prefix `gif-`, or `mimeType ===
image/gif` as `image%2Fgif`, so copied embeds open as GIFs, not PNG.
### 6. Optional — `ReticulumDiscussionDialog.tsx`
Discussions are not the same image-pointer path. This fork fetches the blob
with `fetchLibraryGifFile` and attaches it as a normal file.
---
## Do not copy (mobile-only)
- `src/components/Mobile/MobileEmojiPanel.tsx`
- `src/components/Mobile/emojiPanelBus.ts` (unless you want the no-op stub)
- `src/hooks/useSoftKeyboardInset.ts`
- Capacitor SoftKeyboard / AppUpdate plugins
- Android keyboard / crash-guard bits (`GifLoadCancelled` in `App.tsx` is
nice-to-have only)
---
## Behaviour to keep
- Fetch the catalog once (5 minute TTL). Search and tag chips are client-side
(`filterLibraryGifs`, `gifTagCounts`).
- `gifMediaCache`: concurrency 2, memory + IndexedDB, cancel when a tile
scrolls off screen, resolve cancelled work with an empty string (do not
reject — that became an unhandled rejection on mobile).
- Most-used row from `localStorage` (`gifUsage.ts`, key `qortal-gif-usage-v1`).
- Masonry: three flex columns, `GifImage` `natural` (no square crop).
- Never stampede the node with one fetch per visible tile.
---
## Quick look after cloning this repo
```bash
git clone https://gitea.qortal.link/simon/GO-3.0.git
cd GO-3.0
git log --oneline -20
# Picker + send/render wiring vs upstream Hub
git diff origin/develop -- src/components/Chat/gifs src/components/ReactionPicker.tsx src/utils/chat.ts
```
Voice / dual-ring / `presence_bridge.py` notes are still in
[`HUB_DEV_NOTES.md`](HUB_DEV_NOTES.md) and
[`MOBILE_VOICE_HUB_DEV_NOTE.md`](MOBILE_VOICE_HUB_DEV_NOTE.md). Those are
separate from the GIF picker.