-
Notifications
You must be signed in to change notification settings - Fork 4.1k
feat: stickers panel #539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: stickers panel #539
Conversation
|
@enkeii64 is attempting to deploy a commit to the OpenCut OSS Team on Vercel. A member of the Team first needs to authorize it. |
👷 Deploy request for appcut pending review.Visit the deploys page to approve it
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughA new stickers feature was implemented in the media panel, enabling users to browse, search, and add stickers/icons from Iconify collections. This involved integrating a new StickersView component, creating a Zustand stickers store for state management, and introducing an Iconify API client for icon data retrieval and SVG handling. Minor enhancements were also made to media file loading and project serialization. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant MediaPanel
participant StickersView
participant StickersStore
participant IconifyAPI
participant MediaStore
User->>MediaPanel: Selects "Stickers" tab
MediaPanel->>StickersView: Renders StickersView
StickersView->>StickersStore: Loads collections/searches stickers
StickersStore->>IconifyAPI: Fetch collections/search icons
IconifyAPI-->>StickersStore: Return collections/search results
StickersStore-->>StickersView: Update state with results
User->>StickersView: Clicks "Add" on sticker
StickersView->>StickersStore: downloadSticker(iconName)
StickersStore->>IconifyAPI: Download SVG
IconifyAPI-->>StickersStore: Return SVG as File
StickersStore->>MediaStore: Add sticker file to media library
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Poem
✨ Finishing Touches🧪 Generate 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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (7)
apps/web/src/lib/iconify-api.ts (2)
11-18: Prefer currentHost first in the fallback order to avoid unnecessary retriesAlways trying ICONIFY_HOSTS in the same order can add avoidable latency if a previous call already succeeded on a secondary host. Start with currentHost, then try the others.
- for (const host of ICONIFY_HOSTS) { + const hosts = [currentHost, ...ICONIFY_HOSTS.filter((h) => h !== currentHost)]; + for (const host of hosts) { try { const response = await fetch(`${host}${path}`, { signal: AbortSignal.timeout(2000), }); if (response.ok) { currentHost = host; return response; } } catch (error) { console.warn(`Failed to fetch from ${host}:`, error); } }
14-15: AbortSignal.timeout may not be universally supported in older browsersThis can throw in older Safari. Consider a small fallback using AbortController + setTimeout if AbortSignal.timeout is unavailable.
apps/web/src/lib/storage/storage-service.ts (1)
164-179: Avoid reading the entire file just to sniff SVG; handle XML prolog tooReading the whole blob for detection is wasteful. Sniff a small slice and tolerate XML prolog/comments before .
- if (metadata.type === "image" && (!file.type || file.type === "")) { + if (metadata.type === "image" && (!file.type || file.type === "")) { try { - const text = await file.text(); - if (text.trim().startsWith("<svg")) { + const head = await file.slice(0, 512).text(); + const trimmed = head.trimStart(); + if (/<svg[\s>]/i.test(trimmed)) { const svgBlob = new Blob([text], { type: "image/svg+xml" }); url = URL.createObjectURL(svgBlob); } else { url = URL.createObjectURL(file); } } catch { url = URL.createObjectURL(file); } } else { url = URL.createObjectURL(file); }apps/web/src/components/editor/media-panel/views/stickers.tsx (4)
332-334: Avoidas anyby narrowing the tab value to the union typeKeeps type safety and prevents invalid categories.
- <Tabs - value={selectedCategory} - onValueChange={(v) => setSelectedCategory(v as any)} - > + <Tabs + value={selectedCategory} + onValueChange={(v) => + setSelectedCategory(v as "all" | "general" | "brands" | "emoji") + } + >
292-301: Add accessible name to the clear-search icon buttonIcon-only buttons need an aria-label.
- <button + <button + aria-label="Clear search" className="absolute right-1 top-1/2 -translate-y-1/2 h-6 w-6 p-0 rounded hover:bg-accent flex items-center justify-center" onClick={() => { setLocalSearchQuery(""); setSearchQuery(""); setViewMode("browse"); }} >
365-371: Add accessible name to the clear-recents icon buttonImproves screen-reader usability.
- <button + <button + aria-label="Clear recent stickers" onClick={clearRecentStickers} className="ml-auto h-5 w-5 p-0 rounded hover:bg-accent flex items-center justify-center" >
73-146: Reduce TooltipProvider duplicationTooltipProvider is relatively heavy; wrapping the list once (outside StickerItem) avoids creating a provider per item.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/src/components/editor/media-panel/index.tsx(2 hunks)apps/web/src/components/editor/media-panel/views/stickers.tsx(1 hunks)apps/web/src/lib/iconify-api.ts(1 hunks)apps/web/src/lib/storage/storage-service.ts(3 hunks)apps/web/src/stores/stickers-store.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
PR: OpenCut-app/OpenCut#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Include caption tracks for audio and video elements.
Applied to files:
apps/web/src/components/editor/media-panel/index.tsxapps/web/src/components/editor/media-panel/views/stickers.tsx
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
PR: OpenCut-app/OpenCut#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Don't include "image", "picture", or "photo" in img alt prop.
Applied to files:
apps/web/src/components/editor/media-panel/index.tsx
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
PR: OpenCut-app/OpenCut#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Use semantic elements instead of role attributes in JSX.
Applied to files:
apps/web/src/components/editor/media-panel/index.tsx
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
PR: OpenCut-app/OpenCut#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Use `<>...</>` instead of `<Fragment>...</Fragment>`.
Applied to files:
apps/web/src/components/editor/media-panel/index.tsx
📚 Learning: 2025-08-09T09:03:49.797Z
Learnt from: CR
PR: OpenCut-app/OpenCut#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-08-09T09:03:49.797Z
Learning: Applies to **/*.{jsx,tsx} : Don't use `<img>` elements in Next.js projects.
Applied to files:
apps/web/src/components/editor/media-panel/index.tsx
🔇 Additional comments (2)
apps/web/src/components/editor/media-panel/index.tsx (1)
8-8: Integrates StickersView correctlyImport + view map wiring looks good. Non-breaking, scoped to the stickers tab.
Also applies to: 20-20
apps/web/src/stores/stickers-store.ts (1)
49-189: Solid store structure and error handlingState shape, actions, and async flows (including recent stickers) look clean and predictable. Good job keeping UI concerns out of the store.
| <img | ||
| src={ | ||
| hostIndex === 0 | ||
| ? getIconSvgUrl(iconName, { width: 64, height: 64 }) | ||
| : buildIconSvgUrl( | ||
| ICONIFY_HOSTS[ | ||
| Math.min(hostIndex, ICONIFY_HOSTS.length - 1) | ||
| ], | ||
| iconName, | ||
| { width: 64, height: 64 } | ||
| ) | ||
| } | ||
| alt={displayName} | ||
| className="w-full h-full object-contain" | ||
| onError={() => { | ||
| const next = hostIndex + 1; | ||
| if (next < ICONIFY_HOSTS.length) { | ||
| setHostIndex(next); | ||
| } else { | ||
| setImageError(true); | ||
| } | ||
| }} | ||
| loading="lazy" | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with Next.js
per project guidelines and to enable optimizations
Organizational standard: “Don’t use elements in Next.js projects.” Switch to next/image and add the import. For SVGs from remote hosts, set unoptimized and ensure hosts are permitted in next.config.
+import Image from "next/image";
@@
- <img
+ <Image
src={
hostIndex === 0
? getIconSvgUrl(iconName, { width: 64, height: 64 })
: buildIconSvgUrl(
ICONIFY_HOSTS[
Math.min(hostIndex, ICONIFY_HOSTS.length - 1)
],
iconName,
{ width: 64, height: 64 }
)
}
alt={displayName}
+ width={64}
+ height={64}
className="w-full h-full object-contain"
onError={() => {
const next = hostIndex + 1;
if (next < ICONIFY_HOSTS.length) {
setHostIndex(next);
} else {
setImageError(true);
}
}}
- loading="lazy"
- />
+ loading="lazy"
+ unoptimized
+ />Also add allowed remote patterns (api.iconify.design, api.simplesvg.com, api.unisvg.com) in next.config images.remotePatterns.
I can provide the next.config addition if helpful.
📝 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.
| <img | |
| src={ | |
| hostIndex === 0 | |
| ? getIconSvgUrl(iconName, { width: 64, height: 64 }) | |
| : buildIconSvgUrl( | |
| ICONIFY_HOSTS[ | |
| Math.min(hostIndex, ICONIFY_HOSTS.length - 1) | |
| ], | |
| iconName, | |
| { width: 64, height: 64 } | |
| ) | |
| } | |
| alt={displayName} | |
| className="w-full h-full object-contain" | |
| onError={() => { | |
| const next = hostIndex + 1; | |
| if (next < ICONIFY_HOSTS.length) { | |
| setHostIndex(next); | |
| } else { | |
| setImageError(true); | |
| } | |
| }} | |
| loading="lazy" | |
| /> | |
| // At the top of apps/web/src/components/editor/media-panel/views/stickers.tsx | |
| import Image from "next/image"; | |
| // … other imports and code … | |
| <Image | |
| src={ | |
| hostIndex === 0 | |
| ? getIconSvgUrl(iconName, { width: 64, height: 64 }) | |
| : buildIconSvgUrl( | |
| ICONIFY_HOSTS[ | |
| Math.min(hostIndex, ICONIFY_HOSTS.length - 1) | |
| ], | |
| iconName, | |
| { width: 64, height: 64 } | |
| ) | |
| } | |
| alt={displayName} | |
| width={64} | |
| height={64} | |
| className="w-full h-full object-contain" | |
| onError={() => { | |
| const next = hostIndex + 1; | |
| if (next < ICONIFY_HOSTS.length) { | |
| setHostIndex(next); | |
| } else { | |
| setImageError(true); | |
| } | |
| }} | |
| loading="lazy" | |
| unoptimized | |
| /> |
🤖 Prompt for AI Agents
In apps/web/src/components/editor/media-panel/views/stickers.tsx around lines 88
to 111, replace the <img> element with Next.js <Image> component by importing it
from 'next/image'. Set the unoptimized prop to true for remote SVGs and keep the
existing props like src, alt, className, onError, and loading. Also, update
next.config.js to include the allowed remotePatterns for api.iconify.design,
api.simplesvg.com, and api.unisvg.com under images.remotePatterns to permit
these hosts for image optimization.
| if (params?.color) { | ||
| urlParams.append("color", params.color.replace("#", "%23")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double-encoding bug for color parameter
Manually replacing “#” with “%23” and then passing through URLSearchParams produces “%2523...” in the final URL. Let URLSearchParams handle encoding.
- if (params?.color) {
- urlParams.append("color", params.color.replace("#", "%23"));
- }
+ if (params?.color) {
+ urlParams.append("color", params.color);
+ }🤖 Prompt for AI Agents
In apps/web/src/lib/iconify-api.ts around lines 156 to 158, the code manually
replaces "#" with "%23" in the color parameter before appending it to
URLSearchParams, causing double encoding ("%2523"). Remove the manual
replacement and append the raw color string directly to urlParams so that
URLSearchParams can handle encoding correctly.
| export async function downloadSvgAsText( | ||
| iconName: string, | ||
| params?: Parameters<typeof getIconSvgUrl>[1] | ||
| ): Promise<string> { | ||
| const url = getIconSvgUrl(iconName, params); | ||
| const response = await fetch(url); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to download SVG: ${response.statusText}`); | ||
| } | ||
| return await response.text(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Download should use the same host-fallback with timeout
This currently fetches a single URL without fallback or timeout. Use fetchWithFallback on the icon path to match the resilience used elsewhere.
-export async function downloadSvgAsText(
- iconName: string,
- params?: Parameters<typeof getIconSvgUrl>[1]
-): Promise<string> {
- const url = getIconSvgUrl(iconName, params);
- const response = await fetch(url);
- if (!response.ok) {
- throw new Error(`Failed to download SVG: ${response.statusText}`);
- }
- return await response.text();
-}
+export async function downloadSvgAsText(
+ iconName: string,
+ params?: Parameters<typeof getIconSvgUrl>[1]
+): Promise<string> {
+ const [prefix, name] = iconName.includes(":")
+ ? iconName.split(":")
+ : ["", iconName];
+ if (!prefix || !name) {
+ throw new Error('Invalid icon name format. Expected "prefix:name"');
+ }
+ const urlParams = new URLSearchParams();
+ if (params?.color) urlParams.append("color", params.color);
+ if (params?.width) urlParams.append("width", params.width.toString());
+ if (params?.height) urlParams.append("height", params.height.toString());
+ if (params?.flip) urlParams.append("flip", params.flip);
+ if (params?.rotate) urlParams.append("rotate", params.rotate.toString());
+ const queryString = urlParams.toString();
+ const path = `/${prefix}/${name}.svg${queryString ? "?" + queryString : ""}`;
+
+ const response = await fetchWithFallback(path);
+ if (!response.ok) {
+ throw new Error(`Failed to download SVG: ${response.statusText}`);
+ }
+ return await response.text();
+}📝 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.
| export async function downloadSvgAsText( | |
| iconName: string, | |
| params?: Parameters<typeof getIconSvgUrl>[1] | |
| ): Promise<string> { | |
| const url = getIconSvgUrl(iconName, params); | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`Failed to download SVG: ${response.statusText}`); | |
| } | |
| return await response.text(); | |
| } | |
| export async function downloadSvgAsText( | |
| iconName: string, | |
| params?: Parameters<typeof getIconSvgUrl>[1] | |
| ): Promise<string> { | |
| const [prefix, name] = iconName.includes(":") | |
| ? iconName.split(":") | |
| : ["", iconName]; | |
| if (!prefix || !name) { | |
| throw new Error('Invalid icon name format. Expected "prefix:name"'); | |
| } | |
| const urlParams = new URLSearchParams(); | |
| if (params?.color) urlParams.append("color", params.color); | |
| if (params?.width) urlParams.append("width", params.width.toString()); | |
| if (params?.height) urlParams.append("height", params.height.toString()); | |
| if (params?.flip) urlParams.append("flip", params.flip); | |
| if (params?.rotate) urlParams.append("rotate", params.rotate.toString()); | |
| const queryString = urlParams.toString(); | |
| const path = `/${prefix}/${name}.svg${queryString ? "?" + queryString : ""}`; | |
| const response = await fetchWithFallback(path); | |
| if (!response.ok) { | |
| throw new Error(`Failed to download SVG: ${response.statusText}`); | |
| } | |
| return await response.text(); | |
| } |
🤖 Prompt for AI Agents
In apps/web/src/lib/iconify-api.ts around lines 187 to 197, the
downloadSvgAsText function uses a simple fetch call without fallback or timeout,
which reduces resilience. Replace the fetch call with fetchWithFallback using
the icon URL to implement the same host-fallback and timeout logic used
elsewhere. This will ensure the download is more robust by trying alternative
hosts and respecting timeouts.
| canvasSize: project.canvasSize, | ||
| canvasMode: project.canvasMode, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Back-compat defaults for new project fields (canvasSize/canvasMode)
Loading older projects saved before these fields existed may yield undefined. Ensure TProject tolerates that or provide sane defaults on load.
If TProject requires non-optional values, consider defaulting here:
- canvasSize: serializedProject.canvasSize,
- canvasMode: serializedProject.canvasMode,
+ canvasSize: serializedProject.canvasSize ?? /* get project default here */,
+ canvasMode: serializedProject.canvasMode ?? /* get project default here */,To verify impact across the repo, run:
Also applies to: 101-103
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "TProject definition (first match with context):"
rg -n -A 20 -B 5 $'\\b(type|interface)\\s+TProject\\b'
echo
echo "Usages of canvasSize/canvasMode:"
rg -n 'canvasSize|canvasMode' -A 2 -B 2Length of output: 10674
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Context around serializedProject usage:"
rg -n 'serializedProject' -C5 apps/web/src/lib/storage/storage-service.tsLength of output: 1779
Apply back-compat defaults for canvasSize/canvasMode in loadProject
In apps/web/src/lib/storage/storage-service.ts, the loadProject method returns a TProject whose canvasSize and canvasMode are required by the TProject interface but may be undefined when loading older serialized data. Add safe defaults here:
• File: apps/web/src/lib/storage/storage-service.ts
Location: return block of async loadProject(...) (around lines 101–103)
Suggested change:
return {
id: serializedProject.id,
name: serializedProject.name,
thumbnail: serializedProject.thumbnail,
createdAt: new Date(serializedProject.createdAt),
updatedAt: new Date(serializedProject.updatedAt),
backgroundColor: serializedProject.backgroundColor,
backgroundType: serializedProject.backgroundType,
blurIntensity: serializedProject.blurIntensity,
bookmarks: serializedProject.bookmarks,
fps: serializedProject.fps,
- canvasSize: serializedProject.canvasSize,
- canvasMode: serializedProject.canvasMode,
+ canvasSize: serializedProject.canvasSize ?? DEFAULT_CANVAS_SIZE,
+ canvasMode: serializedProject.canvasMode ?? "preset",
};Don’t forget to import DEFAULT_CANVAS_SIZE (and any mode‐default constant) at the top of the file so these fallbacks resolve correctly.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/src/lib/storage/storage-service.ts around lines 101 to 103 in the
return block of the async loadProject method, canvasSize and canvasMode may be
undefined for older serialized data but are required by the TProject interface.
Fix this by applying safe default values for canvasSize and canvasMode using
DEFAULT_CANVAS_SIZE and a suitable default mode constant. Also, ensure you
import DEFAULT_CANVAS_SIZE and the mode default constant at the top of the file
so these fallbacks resolve correctly.
|
I think this pr is not suitable for the sticker purpose. I've checked the CapCut editor, and they use sticker instead of svg icon you can refer to stickerl |
This is understandable. I do believe the "stickers" from iconify.design are more like icons, but I couldn't find a free (and maybe open source) stickers API. If there is one, someone can change the API or simply add another source for these stickers, so there will be an even wider range of sticker available, and not just mainly icons. I still believe the iconify API is a good choice, as there are over 200,000 icons to choose from. If you know the link you provided has an API, feel free to make a pull request on those changes. |
|
how about tenor api? |
Looks like it's only for GIFs |
|
imo this is better then capcut's stickers throughout my 3 years of video editing, i've needed "app icons" (eg, javascript logo, facebook) and all the other cool iconify stickers than all the stickers capcut has (i genuinely haven't seen anyone use those) |



Description
This PR includes the stickers panel from the Iconify API. You can browse, search, and add the stickers to your OpenCut project.
Fixes # (issue)
No existing issues were fixed.
Type of change
Please delete options that are not relevant.
How Has This Been Tested?
Test manually in the browser. Stickers are visible in the project, in the browser, and render properly.
Test Configuration:
Screenshots (if applicable)
Checklist:
Additional context
Add any other context about the pull request here.
Summary by CodeRabbit
New Features
Bug Fixes
Chores