GIF Sticker Implementation - nself-org/nchat GitHub Wiki

GIF and Sticker Support Implementation Summary

Version: 0.3.0 Date: January 30, 2026 Status: ✅ Complete


Overview

This document provides a comprehensive summary of the GIF and sticker support implementation for nself-chat v0.3.0. The implementation adds full support for:

  1. GIF Search & Sending - Powered by Tenor API
  2. Custom Sticker Packs - Create, manage, and use custom stickers
  3. Rich Message Display - Inline rendering of GIFs and stickers
  4. Admin Management - Full CRUD for sticker packs (admin/owner only)

Features Implemented

1. GIF Search Integration (Tenor API)

  • ✅ Search GIFs via Tenor API
  • ✅ Trending GIFs and search terms
  • ✅ Autocomplete suggestions
  • ✅ Category browsing
  • ✅ Infinite scroll pagination
  • ✅ Preview on hover
  • ✅ Share tracking (analytics)

2. Sticker Packs Management

  • ✅ Create custom sticker packs
  • ✅ Upload stickers (PNG, JPG, GIF, WebP, SVG)
  • ✅ Keyword-based search
  • ✅ Pack organization (tabs)
  • ✅ Default packs (Reactions, Emoji)
  • ✅ Admin-only management UI
  • ✅ Enable/disable packs

3. Message Composer Integration

  • ✅ GIF picker button in message input
  • ✅ Sticker picker button in message input
  • ✅ Popover UI for both pickers
  • ✅ Send GIF messages
  • ✅ Send sticker messages
  • ✅ Feature flags for enable/disable

4. Message Display

  • ✅ Render GIF messages inline
  • ✅ Render sticker messages inline
  • ✅ Loading states
  • ✅ Error handling
  • ✅ Responsive sizing
  • ✅ Optional caption for GIFs

Files Created

Database Migration

File: .backend/migrations/012_gifs_stickers.sql

  • Creates nchat_sticker_packs table
  • Creates nchat_stickers table
  • Adds gif_url, sticker_id, gif_metadata columns to nchat_messages
  • Updates message type enum to include 'gif' and 'sticker'
  • Seeds default sticker packs (Reactions, Emoji)

Tenor API Client

File: src/lib/tenor-client.ts

// Core functionality
;-search(query, limit, pos, contentFilter) -
  featured(limit, pos, contentFilter) -
  trendingTerms(limit) -
  autocomplete(query, limit) -
  categories(type) -
  registerShare(gifId) -
  // Helper methods
  getDisplayUrl(gif, size) -
  getThumbnailUrl(gif) -
  getDimensions(gif)

React Components

GifPicker Component

File: src/components/chat/GifPicker.tsx

  • Search input with debounce
  • Trending terms chips
  • Grid layout (2 columns)
  • Infinite scroll
  • Preview on hover
  • Tenor branding footer

StickerPicker Component

File: src/components/chat/StickerPicker.tsx

  • Search input
  • Tabbed pack navigation
  • Grid layout (4 columns)
  • Keyword filtering
  • Empty states

StickerPackManager Component

File: src/components/chat/StickerPackManager.tsx

  • Create/edit/delete packs
  • Pack list with stats
  • Admin-only access
  • Modal forms

StickerUpload Component

File: src/components/chat/StickerUpload.tsx

  • Drag & drop upload
  • Bulk upload support
  • Edit name, slug, keywords
  • Upload progress tracking
  • Error handling

Hooks

useGifSearch

File: src/hooks/use-gif-search.ts

{
  gifs: TenorGif[]
  isLoading: boolean
  error: string | null
  hasMore: boolean
  trendingTerms: string[]
  loadMore: () => void
  isConfigured: boolean
}

useStickers

File: src/hooks/use-stickers.ts

{
  packs: StickerPack[]
  isLoading: boolean
  error: Error | null
  refetch: () => void
}

useStickerPacksManagement

File: src/hooks/use-sticker-packs.ts

{
  createPack: (input) => Promise<any>
  updatePack: (id, input) => Promise<any>
  deletePack: (id) => Promise<any>
  addSticker: (input) => Promise<any>
  updateSticker: (id, input) => Promise<any>
  deleteSticker: (id) => Promise<any>
  isLoading: boolean
  canManage: boolean
}

GraphQL Mutations

File: src/graphql/mutations/sticker-packs.ts

  • CREATE_STICKER_PACK
  • UPDATE_STICKER_PACK
  • DELETE_STICKER_PACK
  • ADD_STICKER_TO_PACK
  • UPDATE_STICKER
  • DELETE_STICKER
  • SEND_GIF_MESSAGE
  • SEND_STICKER_MESSAGE

Updated Components

MessageInput

File: src/components/chat/message-input.tsx

  • Added GIF picker button (ImageGif icon)
  • Added Sticker picker button (Sticker icon)
  • Added onSendGif prop
  • Added onSendSticker prop
  • Added feature flags for GIFs and stickers
  • Integrated GifPicker and StickerPicker in popovers

MessageContent

File: src/components/chat/message-content.tsx

  • Added type prop (text, gif, sticker, etc.)
  • Added gifUrl and gifMetadata props
  • Added sticker prop
  • Created GifContent component
  • Created StickerContent component
  • Responsive sizing and loading states

Database Schema

nchat_sticker_packs

CREATE TABLE nchat.nchat_sticker_packs (
    id UUID PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE NOT NULL,
    description TEXT,
    icon_url TEXT,
    creator_id UUID NOT NULL REFERENCES nchat.nchat_users(id),
    is_default BOOLEAN DEFAULT FALSE,
    is_enabled BOOLEAN DEFAULT TRUE,
    sort_order INTEGER DEFAULT 0,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

nchat_stickers

CREATE TABLE nchat.nchat_stickers (
    id UUID PRIMARY KEY,
    pack_id UUID NOT NULL REFERENCES nchat.nchat_sticker_packs(id) ON DELETE CASCADE,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) NOT NULL,
    file_url TEXT NOT NULL,
    thumbnail_url TEXT,
    keywords TEXT[],
    sort_order INTEGER DEFAULT 0,
    metadata JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(pack_id, slug)
);

nchat_messages (updated)

-- New columns
ALTER TABLE nchat.nchat_messages ADD COLUMN gif_url TEXT;
ALTER TABLE nchat.nchat_messages ADD COLUMN sticker_id UUID REFERENCES nchat.nchat_stickers(id);
ALTER TABLE nchat.nchat_messages ADD COLUMN gif_metadata JSONB DEFAULT '{}';

-- Updated type constraint
ALTER TABLE nchat.nchat_messages DROP CONSTRAINT valid_type;
ALTER TABLE nchat.nchat_messages ADD CONSTRAINT valid_type
    CHECK (type IN ('text', 'file', 'image', 'video', 'audio', 'system', 'gif', 'sticker'));

Environment Variables

.env.example (updated)

# GIF picker integration (Tenor API)
NEXT_PUBLIC_FEATURE_GIF_PICKER=true

# Sticker packs (custom stickers)
NEXT_PUBLIC_FEATURE_STICKERS=true

# Tenor API Key (for GIF search)
# Get a free API key at https://developers.google.com/tenor/guides/quickstart
# NEXT_PUBLIC_TENOR_API_KEY=

.env.local (add this)

# Get your Tenor API key at https://developers.google.com/tenor/guides/quickstart
NEXT_PUBLIC_TENOR_API_KEY=your-api-key-here

Usage Examples

1. Sending a GIF Message

import { useMutation } from '@apollo/client'
import { SEND_GIF_MESSAGE } from '@/graphql/mutations/sticker-packs'

const [sendGif] = useMutation(SEND_GIF_MESSAGE)

const handleSendGif = async (gif: TenorGif) => {
  await sendGif({
    variables: {
      channel_id: channelId,
      user_id: userId,
      gif_url: tenorClient.getDisplayUrl(gif, 'medium'),
      gif_metadata: {
        width: gif.media_formats.gif?.dims[0],
        height: gif.media_formats.gif?.dims[1],
        preview: tenorClient.getThumbnailUrl(gif),
        title: gif.title,
      },
      content: '', // Optional caption
    },
  })

  // Track share with Tenor for analytics
  tenorClient.registerShare(gif.id)
}

2. Sending a Sticker Message

import { useMutation } from '@apollo/client'
import { SEND_STICKER_MESSAGE } from '@/graphql/mutations/sticker-packs'

const [sendSticker] = useMutation(SEND_STICKER_MESSAGE)

const handleSendSticker = async (sticker: Sticker) => {
  await sendSticker({
    variables: {
      channel_id: channelId,
      user_id: userId,
      sticker_id: sticker.id,
    },
  })
}

3. Creating a Sticker Pack (Admin)

import { useStickerPacksManagement } from '@/hooks/use-sticker-packs'

const { createPack, addSticker } = useStickerPacksManagement()

// Create pack
const pack = await createPack({
  name: 'My Custom Pack',
  slug: 'my-custom-pack',
  description: 'Custom stickers for our team',
})

// Add stickers to pack
await addSticker({
  pack_id: pack.id,
  name: 'Happy Face',
  slug: 'happy-face',
  file_url: 'https://example.com/stickers/happy.png',
  keywords: ['happy', 'smile', 'joy'],
})

4. Integrating in Message Input

<MessageInput
  channelId={channelId}
  onSend={handleSendMessage}
  onSendGif={handleSendGif}
  onSendSticker={handleSendSticker}
  // ... other props
/>

Testing Checklist

GIF Functionality

  • GIF picker opens when clicking GIF button
  • Search returns relevant GIFs
  • Trending terms display correctly
  • GIF preview works on hover
  • Clicking GIF sends message
  • GIF displays inline in message
  • Loading states work
  • Error states handle gracefully
  • Works without Tenor API key (shows config message)

Sticker Functionality

  • Sticker picker opens when clicking sticker button
  • Tabs show different packs
  • Search filters stickers by keyword
  • Clicking sticker sends message
  • Sticker displays inline in message
  • Admin can create packs
  • Admin can upload stickers
  • Admin can edit/delete packs
  • Non-admin users cannot access management UI
  • Default packs are created on migration

Message Display

  • GIF messages render with correct size
  • Sticker messages render at 128x128
  • Loading spinners show while loading
  • Error messages show on load failure
  • Optional GIF caption displays
  • Messages work in threads
  • Messages work in DMs

Default Sticker Packs

The migration creates two default sticker packs:

1. Reactions Pack

  • Thumbs Up 👍
  • Heart ❤️
  • Fire 🔥
  • Celebrate 🎉
  • Thinking 🤔
  • Check ✅

2. Emoji Pack

  • Smile 🙂
  • Joy 😂
  • Cry 😢
  • Rocket 🚀
  • Eyes 👀
  • Clap 👏

Note: These use SVG data URLs with emoji characters as placeholders. In production, replace with actual sticker images.


API Integration Details

Tenor API

Base URL: https://tenor.googleapis.com/v2

Required API Key: Get from Google Developers Console

Endpoints Used:

  • /search - Search GIFs by query
  • /featured - Get trending/featured GIFs
  • /trending_terms - Get trending search terms
  • /autocomplete - Get search suggestions
  • /categories - Get GIF categories
  • /registershare - Track GIF usage (analytics)

Rate Limits: Free tier allows reasonable usage. See Tenor API docs for limits.


Performance Considerations

  1. Image Loading:

    • GIFs use progressive loading with preview URLs
    • Lazy loading for images
    • Proper sizing to avoid layout shifts
  2. API Calls:

    • Debounced search (500ms)
    • Pagination for infinite scroll
    • Client-side caching via Apollo
  3. Database:

    • Indexed keywords for fast sticker search
    • Indexed pack_id for quick lookups
    • JSONB for flexible metadata
  4. Storage:

    • Stickers stored as base64 data URLs (demo)
    • Production: Upload to MinIO/S3 and store URLs
    • Thumbnail generation recommended for large images

Security Considerations

  1. Admin-Only Management:

    • Sticker pack CRUD restricted to owner/admin roles
    • GraphQL permissions configured in Hasura
    • Client-side permission checks via canManage
  2. File Upload:

    • File type validation (PNG, JPG, GIF, WebP, SVG only)
    • File size limit (5MB per sticker)
    • Content moderation recommended for production
  3. Tenor API:

    • Content filter set to 'medium' by default
    • Configurable per request
    • API key exposed to client (public key, safe)

Future Enhancements

Planned Features

  1. Storage Integration:

    • Upload stickers to MinIO/S3
    • Generate thumbnails automatically
    • CDN integration for faster loading
  2. Advanced Management:

    • Bulk sticker operations
    • Pack reordering (drag & drop)
    • Sticker usage analytics
    • Popular stickers tracking
  3. User Experience:

    • Recent stickers (frecency algorithm)
    • Favorite stickers
    • Sticker skin tones
    • Animated sticker support
  4. Integration:

    • Import stickers from Slack
    • Import emoji from Discord
    • Export sticker packs
  5. Mobile:

    • Native sticker picker for React Native
    • Haptic feedback
    • Sticker keyboard integration

Migration Instructions

1. Run Database Migration

# Navigate to backend directory
cd .backend

# Apply migration
docker exec -i $(docker ps -qf "name=postgres") psql -U postgres -d nchat < migrations/012_gifs_stickers.sql

# Or use Hasura Console
# Upload migration via Hasura Console > Data > SQL

2. Add Environment Variables

# Get Tenor API key
# Visit https://developers.google.com/tenor/guides/quickstart
# Sign up and create an API key

# Add to .env.local
echo "NEXT_PUBLIC_TENOR_API_KEY=your-api-key-here" >> .env.local

3. Install Dependencies (if needed)

All required dependencies are already in package.json:

  • react-dropzone (file upload)
  • emoji-picker-react (emoji picker)
  • @apollo/client (GraphQL)

4. Test Implementation

# Start development server
pnpm dev

# Navigate to chat
# Click GIF button (should open GIF picker)
# Click Sticker button (should show default packs)

# Test as admin/owner
# Go to /admin/stickers (if route exists)
# Or use StickerPackManager component directly

Troubleshooting

GIF Picker Shows "Not Configured"

Problem: Tenor API key is not set Solution: Add NEXT_PUBLIC_TENOR_API_KEY to .env.local

Stickers Not Displaying

Problem: Migration not run or default packs missing Solution: Run migration 012_gifs_stickers.sql

Upload Failing

Problem: File size too large or wrong format Solution: Check file is < 5MB and is PNG/JPG/GIF/WebP/SVG

Permission Denied

Problem: User is not admin/owner Solution: Check user role in nchat_users table

GIF Not Loading

Problem: Tenor API rate limit or network issue Solution: Check browser console, verify API key is valid


Production Deployment

Pre-Deployment Checklist

  • Run database migration on production database
  • Add NEXT_PUBLIC_TENOR_API_KEY to production env
  • Configure storage backend (MinIO/S3) for stickers
  • Update StickerUpload to use real storage
  • Set up CDN for sticker delivery
  • Configure content moderation (if needed)
  • Test with production data
  • Update Hasura permissions
  • Monitor API usage (Tenor rate limits)

Deployment Steps

  1. Database: Apply migration to production PostgreSQL
  2. Environment: Set Tenor API key in hosting platform
  3. Build: Run pnpm build with production env
  4. Deploy: Deploy to Vercel/Netlify/Docker
  5. Verify: Test GIF and sticker functionality
  6. Monitor: Check Sentry for errors, Tenor API usage

Support & Resources

Documentation

Getting Help

  • GitHub Issues: Report bugs or request features
  • Discord: Join nself community
  • Email: [email protected]

Changelog

v0.3.0 (2026-01-30)

Added:

  • GIF search and sending via Tenor API
  • Custom sticker pack support
  • Sticker upload and management (admin)
  • GIF and sticker rendering in messages
  • 2 default sticker packs (Reactions, Emoji)
  • GraphQL mutations for GIFs and stickers
  • Comprehensive hooks and components
  • Feature flags for GIFs and stickers

Database:

  • Added nchat_sticker_packs table
  • Added nchat_stickers table
  • Updated nchat_messages with GIF/sticker columns
  • Added 12 default stickers across 2 packs

Components:

  • Created GifPicker.tsx
  • Created StickerPicker.tsx
  • Created StickerPackManager.tsx
  • Created StickerUpload.tsx
  • Updated MessageInput.tsx
  • Updated MessageContent.tsx

Hooks:

  • Created use-gif-search.ts
  • Created use-stickers.ts
  • Created use-sticker-packs.ts

Libraries:

  • Created tenor-client.ts

Credits

Implementation: AI assistant Project: nself-chat v0.3.0 Framework: Next.js 15 + React 19 Backend: nself CLI v0.4.2 GIF API: Tenor by Google


License

See main project LICENSE file.

⚠️ **GitHub.com Fallback** ⚠️