feat(chat): add GIF picker with Giphy integration#127
Conversation
- Add backend proxy routes for Giphy trending/search (keeps API key server-side) - Add GifPicker component with debounced search and pagination - Wire GIF button into MessageInput, reusing existing image send pipeline - No schema changes; GIFs flow through existing Message.image field Closes CodePlaygroundHub#118
|
Someone is attempting to deploy a commit to the Akash Santra 's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a GIPHY-backed GIF service and protected API endpoints, plus a frontend picker for trending/search results, pagination, error handling, and GIF selection from the message composer. ChangesGIF Picker Integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant MessageInput
participant GifPicker
participant BackendGIFRoutes
participant GIPHYAPI
MessageInput->>GifPicker: Open picker
GifPicker->>BackendGIFRoutes: Request trending or search GIFs
BackendGIFRoutes->>GIPHYAPI: Fetch GIF data
GIPHYAPI-->>BackendGIFRoutes: Return GIF data and pagination
BackendGIFRoutes-->>GifPicker: Return normalized GIF results
GifPicker-->>MessageInput: Select GIF URL
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
backend/src/services/gif.service.js (1)
6-13: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider a smaller GIF variant for the sent message, not the original.
urlmaps toimages.original, which for many Giphy GIFs is multiple MB. Since this URL becomesMessage.imageand goes through the existing upload pipeline, sending a smaller rendition (e.g.fixed_heightordownsized) would reduce bandwidth/storage while keeping visual quality adequate for chat.♻️ Suggested change
const mapGif = (gif) => ({ id: gif.id, title: gif.title, previewUrl: gif.images?.fixed_width_small?.url || gif.images?.fixed_width?.url, - url: gif.images?.original?.url, + url: gif.images?.downsized?.url || gif.images?.fixed_height?.url || gif.images?.original?.url, width: gif.images?.fixed_width?.width, height: gif.images?.fixed_width?.height, });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/src/services/gif.service.js` around lines 6 - 13, Update the url mapping in mapGif to use an appropriate smaller Giphy rendition, such as fixed_height or downsized, instead of images.original; preserve a fallback chain so the message still receives an available GIF URL when the preferred variant is missing.frontend/src/components/GifPicker.jsx (1)
74-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winPage-size
24is duplicated from the backend'sDEFAULT_LIMIT.
offset + 24assumes the backend's page size (gif.service.jsDEFAULT_LIMIT), but the response only returns{ gifs, hasMore }, not the actual count/limit used. If the backend default ever changes, pagination will silently skip or repeat items.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/components/GifPicker.jsx` around lines 74 - 77, Update handleLoadMore to advance pagination using the actual number of GIFs returned by the previous fetch, rather than the hardcoded 24. Track or expose that returned count alongside gifs/hasMore, and use it when calculating the next offset while preserving the existing loading and hasMore guards.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/GifPicker.jsx`:
- Around line 59-72: Prevent the debounce effect from scheduling an initial
fetch when query is empty on mount, while preserving the immediate trending
fetch in the mount effect. Update the effects around fetchGifs and debounceRef
so subsequent query changes still trigger the 400ms debounced search without
duplicate requests.
In `@frontend/src/components/MessageInput.jsx`:
- Around line 443-472: Update the GIF toggle handler in MessageInput to close
the emoji picker when opening GIFs, and update the emoji toggle handler
symmetrically to close the GIF picker when opening emojis. Preserve each
picker’s existing toggle behavior so only one popover can be open at a time.
---
Nitpick comments:
In `@backend/src/services/gif.service.js`:
- Around line 6-13: Update the url mapping in mapGif to use an appropriate
smaller Giphy rendition, such as fixed_height or downsized, instead of
images.original; preserve a fallback chain so the message still receives an
available GIF URL when the preferred variant is missing.
In `@frontend/src/components/GifPicker.jsx`:
- Around line 74-77: Update handleLoadMore to advance pagination using the
actual number of GIFs returned by the previous fetch, rather than the hardcoded
24. Track or expose that returned count alongside gifs/hasMore, and use it when
calculating the next offset while preserving the existing loading and hasMore
guards.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 384da454-3011-4649-ab48-e13afa3a05ac
📒 Files selected for processing (8)
backend/.env.examplebackend/src/controllers/gif.controller.jsbackend/src/index.jsbackend/src/middleware/rateLimiter.jsbackend/src/routes/gif.routes.jsbackend/src/services/gif.service.jsfrontend/src/components/GifPicker.jsxfrontend/src/components/MessageInput.jsx
| // Load trending GIFs on mount | ||
| useEffect(() => { | ||
| fetchGifs("", 0, false); | ||
| }, [fetchGifs]); | ||
|
|
||
| // Debounced search as the user types | ||
| useEffect(() => { | ||
| clearTimeout(debounceRef.current); | ||
| debounceRef.current = setTimeout(() => { | ||
| fetchGifs(query, 0, false); | ||
| }, 400); | ||
|
|
||
| return () => clearTimeout(debounceRef.current); | ||
| }, [query, fetchGifs]); |
There was a problem hiding this comment.
🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win
Duplicate initial trending fetch on mount.
The mount effect (Lines 60-62) fires fetchGifs("", 0, false) immediately. The debounce effect (Lines 65-72) also runs on mount (effects always run once regardless of dependency values) and, since query starts as "", schedules the same trending fetch again ~400ms later. This burns two requests against gifRateLimiter's 30/min budget for a single picker open and can cause a brief loading-spinner flicker.
🐛 Suggested fix
+ const isFirstRenderRef = useRef(true);
+
// Load trending GIFs on mount
useEffect(() => {
fetchGifs("", 0, false);
}, [fetchGifs]);
// Debounced search as the user types
useEffect(() => {
+ if (isFirstRenderRef.current) {
+ isFirstRenderRef.current = false;
+ return;
+ }
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => {
fetchGifs(query, 0, false);
}, 400);
return () => clearTimeout(debounceRef.current);
}, [query, fetchGifs]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Load trending GIFs on mount | |
| useEffect(() => { | |
| fetchGifs("", 0, false); | |
| }, [fetchGifs]); | |
| // Debounced search as the user types | |
| useEffect(() => { | |
| clearTimeout(debounceRef.current); | |
| debounceRef.current = setTimeout(() => { | |
| fetchGifs(query, 0, false); | |
| }, 400); | |
| return () => clearTimeout(debounceRef.current); | |
| }, [query, fetchGifs]); | |
| const isFirstRenderRef = useRef(true); | |
| // Load trending GIFs on mount | |
| useEffect(() => { | |
| fetchGifs("", 0, false); | |
| }, [fetchGifs]); | |
| // Debounced search as the user types | |
| useEffect(() => { | |
| if (isFirstRenderRef.current) { | |
| isFirstRenderRef.current = false; | |
| return; | |
| } | |
| clearTimeout(debounceRef.current); | |
| debounceRef.current = setTimeout(() => { | |
| fetchGifs(query, 0, false); | |
| }, 400); | |
| return () => clearTimeout(debounceRef.current); | |
| }, [query, fetchGifs]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/GifPicker.jsx` around lines 59 - 72, Prevent the
debounce effect from scheduling an initial fetch when query is empty on mount,
while preserving the immediate trending fetch in the mount effect. Update the
effects around fetchGifs and debounceRef so subsequent query changes still
trigger the 400ms debounced search without duplicate requests.
| <div className="relative" ref={gifRef}> | ||
| <button | ||
| type="button" | ||
| onClick={() => setShowGifPicker((prev) => !prev)} | ||
| className="btn btn-ghost btn-circle btn-xs sm:btn-sm" | ||
| > | ||
| <Clapperboard size={18} /> | ||
| </button> | ||
|
|
||
| {showGifPicker && ( | ||
| <div | ||
| className=" | ||
| fixed sm:absolute | ||
| bottom-0 sm:bottom-12 | ||
| left-0 sm:left-auto | ||
| right-0 | ||
| z-50 | ||
| w-full sm:w-auto | ||
| sm:border sm:rounded-xl | ||
| shadow-lg | ||
| " | ||
| > | ||
| <GifPicker | ||
| onSelect={handleGifSelect} | ||
| onClose={() => setShowGifPicker(false)} | ||
| /> | ||
| </div> | ||
| )} | ||
| </div> | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Emoji and GIF pickers can be open simultaneously and overlap.
Opening the GIF picker doesn't close showEmojiPicker (and the existing emoji toggle at Line 406 doesn't close showGifPicker). Both popovers share the same fixed sm:absolute bottom-0 sm:bottom-12 ... positioning, so toggling both open stacks them at the same location.
🐛 Suggested fix
<button
type="button"
- onClick={() => setShowGifPicker((prev) => !prev)}
+ onClick={() => {
+ setShowEmojiPicker(false);
+ setShowGifPicker((prev) => !prev);
+ }}
className="btn btn-ghost btn-circle btn-xs sm:btn-sm"
>
<Clapperboard size={18} />
</button>And symmetrically on the emoji button (Line 406):
<button
type="button"
- onClick={() => setShowEmojiPicker((prev) => !prev)}
+ onClick={() => {
+ setShowGifPicker(false);
+ setShowEmojiPicker((prev) => !prev);
+ }}
className="btn btn-ghost btn-circle btn-xs sm:btn-sm"
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/MessageInput.jsx` around lines 443 - 472, Update the
GIF toggle handler in MessageInput to close the emoji picker when opening GIFs,
and update the emoji toggle handler symmetrically to close the GIF picker when
opening emojis. Preserve each picker’s existing toggle behavior so only one
popover can be open at a time.
What changed
Integrates a GIF picker into the message composer, backed by Giphy, as requested in #118.
/api/gif/trending,/api/gif/search) — keeps the Giphy API key server-side only, never exposed to the client. Protected byprotectRouteand a dedicated rate limiter (30 req/min per IP) to gracefully handle abuse/rate-limit scenarios.GifPickercomponent — trending GIFs load on open, debounced keyword search (400ms), paginated "Load more," and distinct loading/empty/error states.MessageInput, hidden for AI chats just like those.imagePreviewstate and send pipeline — the GIF URL flows throughsendMessage/sendGroupMessageexactly like an uploaded photo, gets uploaded to Cloudinary server-side, and is stored on the existingMessage.imagefield. This means DMs, groups, reactions, replies, and message rendering all work with zero changes tomessage.controller.jsor theMessagemodel.Why this approach
Rather than building a parallel "GIF message" type, this treats a GIF exactly like an image message, since the app already has a complete image-message pipeline (composer → Cloudinary upload → socket emit → render). This kept the diff small and low-risk, per the issue's goal of not disrupting existing messaging functionality.
Testing performed
199 passed, 2 skipped, 0 failed)vite build)npm run lintclean on all new/modified files (no new errors; pre-existing repo-wide warnings unaffected)Screenshots:




Trending
Cat Search
Sent in chat
Sent in group
Known limitations
GIPHY_API_KEYenv var to be configured; without it, the picker shows a graceful "not available" message rather than crashing (documented in.env.example).Closes #118
Summary by CodeRabbit