Skip to content

feat(chat): add GIF picker with Giphy integration#127

Open
Komal290106 wants to merge 1 commit into
CodePlaygroundHub:mainfrom
Komal290106:feat/gif-picker
Open

feat(chat): add GIF picker with Giphy integration#127
Komal290106 wants to merge 1 commit into
CodePlaygroundHub:mainfrom
Komal290106:feat/gif-picker

Conversation

@Komal290106

@Komal290106 Komal290106 commented Jul 20, 2026

Copy link
Copy Markdown

What changed

Integrates a GIF picker into the message composer, backed by Giphy, as requested in #118.

  • Backend proxy (/api/gif/trending, /api/gif/search) — keeps the Giphy API key server-side only, never exposed to the client. Protected by protectRoute and a dedicated rate limiter (30 req/min per IP) to gracefully handle abuse/rate-limit scenarios.
  • GifPicker component — trending GIFs load on open, debounced keyword search (400ms), paginated "Load more," and distinct loading/empty/error states.
  • Composer integration — new GIF button sits alongside the existing emoji/image/file buttons in MessageInput, hidden for AI chats just like those.
  • No schema or migration changes. Selecting a GIF reuses the existing imagePreview state and send pipeline — the GIF URL flows through sendMessage/sendGroupMessage exactly like an uploaded photo, gets uploaded to Cloudinary server-side, and is stored on the existing Message.image field. This means DMs, groups, reactions, replies, and message rendering all work with zero changes to message.controller.js or the Message model.

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

  • ✅ Trending GIFs load on opening the picker
  • ✅ Search by keyword (e.g. "cat") returns relevant, debounced results
  • ✅ Preview/selection before sending
  • ✅ Sent and rendered correctly (animated) in a 1:1 DM
  • ✅ Sent and rendered correctly in a group chat
  • ✅ GIF button correctly hidden in the AI assistant chat, matching existing image/file/emoji/mic behavior
  • ✅ Pagination via "Load more"
  • ✅ Graceful error state when the provider is unreachable/misconfigured
  • ✅ Full backend test suite passes with no regressions (199 passed, 2 skipped, 0 failed)
  • ✅ Frontend build passes (vite build)
  • npm run lint clean on all new/modified files (no new errors; pre-existing repo-wide warnings unaffected)

Screenshots:
Trending
Screenshot 2026-07-20 170243
Cat Search
Screenshot 2026-07-20 171620
Sent in chat
Screenshot 2026-07-20 170949
Sent in group
Screenshot 2026-07-20 171210

Known limitations

  • Requires a GIPHY_API_KEY env var to be configured; without it, the picker shows a graceful "not available" message rather than crashing (documented in .env.example).
  • Currently uses Giphy only (no Tenor fallback), per the issue's "e.g. GIPHY or Tenor" wording — happy to add Tenor as a follow-up if maintainers prefer.
  • Rate limit (30 req/min/IP) is a starting point and can be tuned.

Closes #118

Summary by CodeRabbit

  • New Features
    • Added a GIF picker to message composition with trending GIFs, search, previews, and “Load more” pagination.
    • Selected GIFs can now be attached directly to messages.
    • Added protected GIF browsing and search endpoints powered by GIPHY.
    • Added request rate limiting and clear loading, empty, and error states.
  • Configuration
    • Added a GIPHY API key setting to the environment configuration example.

- 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
Copilot AI review requested due to automatic review settings July 20, 2026 11:59
@vercel

vercel Bot commented Jul 20, 2026

Copy link
Copy Markdown

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

GIF Picker Integration

Layer / File(s) Summary
GIPHY service and configuration
backend/.env.example, backend/src/services/gif.service.js
Adds GIPHY configuration, API key validation, response mapping, pagination, trending retrieval, and search retrieval.
GIF API routes and handlers
backend/src/middleware/rateLimiter.js, backend/src/routes/gif.routes.js, backend/src/controllers/gif.controller.js, backend/src/index.js
Adds protected and rate-limited /api/gif/trending and /api/gif/search endpoints with provider and request error responses.
GIF picker browsing and search
frontend/src/components/GifPicker.jsx
Adds debounced search, trending results, pagination, stale-request protection, loading states, error states, and GIF selection.
Message composer GIF selection
frontend/src/components/MessageInput.jsx
Adds a GIF toolbar button, picker dismissal handling, and selected GIF preview 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
Loading

Suggested labels: enhancement

Suggested reviewers: copilot, akash504-ai

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a Giphy-backed GIF picker to chat.
Linked Issues check ✅ Passed The PR implements trending/search GIF browsing, preview, sending in chats, pagination, and error handling as requested in #118.
Out of Scope Changes check ✅ Passed The changes all support the GIF picker feature and do not introduce unrelated functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
backend/src/services/gif.service.js (1)

6-13: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider a smaller GIF variant for the sent message, not the original.

url maps to images.original, which for many Giphy GIFs is multiple MB. Since this URL becomes Message.image and goes through the existing upload pipeline, sending a smaller rendition (e.g. fixed_height or downsized) 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 win

Page-size 24 is duplicated from the backend's DEFAULT_LIMIT.

offset + 24 assumes the backend's page size (gif.service.js DEFAULT_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

📥 Commits

Reviewing files that changed from the base of the PR and between 64c82d1 and 354d74d.

📒 Files selected for processing (8)
  • backend/.env.example
  • backend/src/controllers/gif.controller.js
  • backend/src/index.js
  • backend/src/middleware/rateLimiter.js
  • backend/src/routes/gif.routes.js
  • backend/src/services/gif.service.js
  • frontend/src/components/GifPicker.jsx
  • frontend/src/components/MessageInput.jsx

Comment on lines +59 to +72
// 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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 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.

Suggested change
// 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.

Comment on lines +443 to +472
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Integrate GIF Picker for Chat Messages

2 participants