GIF Sticker Implementation - nself-org/nchat GitHub Wiki
Version: 0.3.0 Date: January 30, 2026 Status: ✅ Complete
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:
- GIF Search & Sending - Powered by Tenor API
- Custom Sticker Packs - Create, manage, and use custom stickers
- Rich Message Display - Inline rendering of GIFs and stickers
- Admin Management - Full CRUD for sticker packs (admin/owner only)
- ✅ Search GIFs via Tenor API
- ✅ Trending GIFs and search terms
- ✅ Autocomplete suggestions
- ✅ Category browsing
- ✅ Infinite scroll pagination
- ✅ Preview on hover
- ✅ Share tracking (analytics)
- ✅ 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
- ✅ 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
- ✅ Render GIF messages inline
- ✅ Render sticker messages inline
- ✅ Loading states
- ✅ Error handling
- ✅ Responsive sizing
- ✅ Optional caption for GIFs
File: .backend/migrations/012_gifs_stickers.sql
- Creates
nchat_sticker_packstable - Creates
nchat_stickerstable - Adds
gif_url,sticker_id,gif_metadatacolumns tonchat_messages - Updates message type enum to include
'gif'and'sticker' - Seeds default sticker packs (Reactions, Emoji)
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)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
File: src/components/chat/StickerPicker.tsx
- Search input
- Tabbed pack navigation
- Grid layout (4 columns)
- Keyword filtering
- Empty states
File: src/components/chat/StickerPackManager.tsx
- Create/edit/delete packs
- Pack list with stats
- Admin-only access
- Modal forms
File: src/components/chat/StickerUpload.tsx
- Drag & drop upload
- Bulk upload support
- Edit name, slug, keywords
- Upload progress tracking
- Error handling
File: src/hooks/use-gif-search.ts
{
gifs: TenorGif[]
isLoading: boolean
error: string | null
hasMore: boolean
trendingTerms: string[]
loadMore: () => void
isConfigured: boolean
}File: src/hooks/use-stickers.ts
{
packs: StickerPack[]
isLoading: boolean
error: Error | null
refetch: () => void
}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
}File: src/graphql/mutations/sticker-packs.ts
CREATE_STICKER_PACKUPDATE_STICKER_PACKDELETE_STICKER_PACKADD_STICKER_TO_PACKUPDATE_STICKERDELETE_STICKERSEND_GIF_MESSAGESEND_STICKER_MESSAGE
File: src/components/chat/message-input.tsx
- Added GIF picker button (ImageGif icon)
- Added Sticker picker button (Sticker icon)
- Added
onSendGifprop - Added
onSendStickerprop - Added feature flags for GIFs and stickers
- Integrated
GifPickerandStickerPickerin popovers
File: src/components/chat/message-content.tsx
- Added
typeprop (text, gif, sticker, etc.) - Added
gifUrlandgifMetadataprops - Added
stickerprop - Created
GifContentcomponent - Created
StickerContentcomponent - Responsive sizing and loading states
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()
);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)
);-- 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'));# 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=# Get your Tenor API key at https://developers.google.com/tenor/guides/quickstart
NEXT_PUBLIC_TENOR_API_KEY=your-api-key-hereimport { 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)
}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,
},
})
}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'],
})<MessageInput
channelId={channelId}
onSend={handleSendMessage}
onSendGif={handleSendGif}
onSendSticker={handleSendSticker}
// ... other props
/>- 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 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
- 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
The migration creates two default sticker packs:
- Thumbs Up 👍
- Heart ❤️
- Fire 🔥
- Celebrate 🎉
- Thinking 🤔
- Check ✅
- Smile 🙂
- Joy 😂
- Cry 😢
- Rocket 🚀
- Eyes 👀
- Clap 👏
Note: These use SVG data URLs with emoji characters as placeholders. In production, replace with actual sticker images.
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.
-
Image Loading:
- GIFs use progressive loading with preview URLs
- Lazy loading for images
- Proper sizing to avoid layout shifts
-
API Calls:
- Debounced search (500ms)
- Pagination for infinite scroll
- Client-side caching via Apollo
-
Database:
- Indexed keywords for fast sticker search
- Indexed pack_id for quick lookups
- JSONB for flexible metadata
-
Storage:
- Stickers stored as base64 data URLs (demo)
- Production: Upload to MinIO/S3 and store URLs
- Thumbnail generation recommended for large images
-
Admin-Only Management:
- Sticker pack CRUD restricted to owner/admin roles
- GraphQL permissions configured in Hasura
- Client-side permission checks via
canManage
-
File Upload:
- File type validation (PNG, JPG, GIF, WebP, SVG only)
- File size limit (5MB per sticker)
- Content moderation recommended for production
-
Tenor API:
- Content filter set to 'medium' by default
- Configurable per request
- API key exposed to client (public key, safe)
-
Storage Integration:
- Upload stickers to MinIO/S3
- Generate thumbnails automatically
- CDN integration for faster loading
-
Advanced Management:
- Bulk sticker operations
- Pack reordering (drag & drop)
- Sticker usage analytics
- Popular stickers tracking
-
User Experience:
- Recent stickers (frecency algorithm)
- Favorite stickers
- Sticker skin tones
- Animated sticker support
-
Integration:
- Import stickers from Slack
- Import emoji from Discord
- Export sticker packs
-
Mobile:
- Native sticker picker for React Native
- Haptic feedback
- Sticker keyboard integration
# 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# 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.localAll required dependencies are already in package.json:
-
react-dropzone(file upload) -
emoji-picker-react(emoji picker) -
@apollo/client(GraphQL)
# 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 directlyProblem: Tenor API key is not set
Solution: Add NEXT_PUBLIC_TENOR_API_KEY to .env.local
Problem: Migration not run or default packs missing
Solution: Run migration 012_gifs_stickers.sql
Problem: File size too large or wrong format Solution: Check file is < 5MB and is PNG/JPG/GIF/WebP/SVG
Problem: User is not admin/owner
Solution: Check user role in nchat_users table
Problem: Tenor API rate limit or network issue Solution: Check browser console, verify API key is valid
- Run database migration on production database
- Add
NEXT_PUBLIC_TENOR_API_KEYto production env - Configure storage backend (MinIO/S3) for stickers
- Update
StickerUploadto 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)
- Database: Apply migration to production PostgreSQL
- Environment: Set Tenor API key in hosting platform
-
Build: Run
pnpm buildwith production env - Deploy: Deploy to Vercel/Netlify/Docker
- Verify: Test GIF and sticker functionality
- Monitor: Check Sentry for errors, Tenor API usage
- GitHub Issues: Report bugs or request features
- Discord: Join nself community
- Email: [email protected]
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_packstable - Added
nchat_stickerstable - Updated
nchat_messageswith 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
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
See main project LICENSE file.