diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index dae7a43aa3..7fe56db63d 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-latest
env:
VITE_TENOR_API_KEY: ${{ secrets.TENOR_API_KEY }}
+ VITE_KLIPY_API_KEY: ${{ secrets.KLIPY_API_KEY }}
steps:
- name: Checkout repo
diff --git a/CLAUDE.md b/CLAUDE.md
index 1b87f8bcba..9d339cd2dc 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -80,7 +80,8 @@ Packages have complex interdependencies. Key relationships:
## Special Configuration
### Environment Variables
-- `VITE_TENOR_API_KEY`: Required for Gif card functionality
+- `VITE_KLIPY_API_KEY`: Required for Gif card functionality (preferred provider)
+- `VITE_TENOR_API_KEY`: Required for Gif card functionality (fallback when Klipy key is not set)
- `VITE_SENTRY_*`: Error tracking configuration
### External Dependencies
diff --git a/packages/koenig-lexical/README.md b/packages/koenig-lexical/README.md
index 8cd5523a74..ea9ae8c98d 100644
--- a/packages/koenig-lexical/README.md
+++ b/packages/koenig-lexical/README.md
@@ -32,12 +32,14 @@ Now, if you navigate to Ghost Admin at http://localhost:2368/ghost and open a po
#### Gif card
-To see this card locally, you need to create `.env.local` file in `koenig-lexical` root package with the next data:
+To see this card locally, create a `.env.local` file in the `koenig-lexical` root package with a GIF provider key:
```
+VITE_KLIPY_API_KEY=xxx
+# or, for the legacy Tenor provider:
VITE_TENOR_API_KEY=xxx
```
-How to get the tenor key is described here https://ghost.org/docs/config/#tenor
+The card resolves to Klipy when `VITE_KLIPY_API_KEY` is set, otherwise Tenor. Get a Klipy key at https://partner.klipy.com; the Tenor key is described at https://ghost.org/docs/config/#tenor
#### Bookmark & Embed cards
diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx
index 97376bcf74..eb86e4502b 100644
--- a/packages/koenig-lexical/demo/DemoApp.jsx
+++ b/packages/koenig-lexical/demo/DemoApp.jsx
@@ -22,7 +22,7 @@ import {
import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig';
import {fetchEmbed} from './utils/fetchEmbed';
import {fileTypes, useFileUpload} from './utils/useFileUpload';
-import {tenorConfig} from './utils/tenorConfig';
+import {klipyConfig, tenorConfig} from './utils/gifConfig';
import {useLocation, useSearchParams} from 'react-router-dom';
import {useSnippets} from './utils/useSnippets';
@@ -47,6 +47,7 @@ const defaultCardConfig = {
unsplash: defaultUnsplashHeaders,
fetchEmbed: fetchEmbed,
tenor: tenorConfig,
+ klipy: klipyConfig,
fetchAutocompleteLinks: () => Promise.resolve([
{label: 'Homepage', value: window.location.origin + '/'},
{label: 'Free signup', value: window.location.origin + '/#/portal/signup/free'}
diff --git a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx
index 547f9998e3..bb65f9e9da 100644
--- a/packages/koenig-lexical/demo/HtmlOutputDemo.jsx
+++ b/packages/koenig-lexical/demo/HtmlOutputDemo.jsx
@@ -6,13 +6,14 @@ import {$getRoot, $isDecoratorNode} from 'lexical';
import {HtmlOutputPlugin, KoenigComposableEditor, KoenigComposer} from '../src';
import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig';
import {fileTypes, useFileUpload} from './utils/useFileUpload';
-import {tenorConfig} from './utils/tenorConfig';
+import {klipyConfig, tenorConfig} from './utils/gifConfig';
import {useSnippets} from './utils/useSnippets';
import {useState} from 'react';
const cardConfig = {
unsplash: {defaultHeaders: defaultUnsplashHeaders},
- tenor: tenorConfig
+ tenor: tenorConfig,
+ klipy: klipyConfig
};
function HtmlOutputDemo() {
diff --git a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx
index 6002645a4c..1c4b25cd9b 100644
--- a/packages/koenig-lexical/demo/RestrictedContentDemo.jsx
+++ b/packages/koenig-lexical/demo/RestrictedContentDemo.jsx
@@ -6,14 +6,15 @@ import {$getRoot, $isDecoratorNode} from 'lexical';
import {KoenigComposableEditor, KoenigComposer, RestrictContentPlugin} from '../src';
import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig';
import {fileTypes, useFileUpload} from './utils/useFileUpload';
-import {tenorConfig} from './utils/tenorConfig';
+import {klipyConfig, tenorConfig} from './utils/gifConfig';
import {useLocation} from 'react-router-dom';
import {useSnippets} from './utils/useSnippets';
import {useState} from 'react';
const cardConfig = {
unsplash: {defaultHeaders: defaultUnsplashHeaders},
- tenor: tenorConfig
+ tenor: tenorConfig,
+ klipy: klipyConfig
};
function useQuery() {
diff --git a/packages/koenig-lexical/demo/utils/gifConfig.js b/packages/koenig-lexical/demo/utils/gifConfig.js
new file mode 100644
index 0000000000..9991063a1c
--- /dev/null
+++ b/packages/koenig-lexical/demo/utils/gifConfig.js
@@ -0,0 +1,36 @@
+import {isTestEnv} from '../../test/utils/isTestEnv';
+
+export const tenorConfig = isTestEnv ? {googleApiKey: 'xxx'} : getTenorConfig();
+
+// In tests the GIF provider defaults to Tenor; the ?gifProvider=klipy query
+// param opts a specific test into the Klipy path.
+export const klipyConfig = isTestEnv ? getTestKlipyConfig() : getKlipyConfig();
+
+function getTenorConfig() {
+ let config = null;
+
+ if (import.meta.env.VITE_TENOR_API_KEY) {
+ config = {
+ googleApiKey: import.meta.env.VITE_TENOR_API_KEY
+ };
+ }
+
+ return config;
+}
+
+function getKlipyConfig() {
+ let config = null;
+
+ if (import.meta.env.VITE_KLIPY_API_KEY) {
+ config = {
+ apiKey: import.meta.env.VITE_KLIPY_API_KEY
+ };
+ }
+
+ return config;
+}
+
+function getTestKlipyConfig() {
+ const provider = new URLSearchParams(window.location.search).get('gifProvider');
+ return provider === 'klipy' ? {apiKey: 'xxx'} : null;
+}
diff --git a/packages/koenig-lexical/demo/utils/tenorConfig.js b/packages/koenig-lexical/demo/utils/tenorConfig.js
deleted file mode 100644
index f5b653b8a8..0000000000
--- a/packages/koenig-lexical/demo/utils/tenorConfig.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import {isTestEnv} from '../../test/utils/isTestEnv';
-
-export const tenorConfig = isTestEnv ? {googleApiKey: 'xxx'} : getTenorConfig();
-
-function getTenorConfig() {
- let config = null;
-
- if (import.meta.env.VITE_TENOR_API_KEY) {
- config = {
- googleApiKey: import.meta.env.VITE_TENOR_API_KEY
- };
- }
-
- return config;
-}
diff --git a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx b/packages/koenig-lexical/src/components/ui/GifPlugin.jsx
similarity index 68%
rename from packages/koenig-lexical/src/components/ui/TenorPlugin.jsx
rename to packages/koenig-lexical/src/components/ui/GifPlugin.jsx
index d99033451b..0763ff3f27 100644
--- a/packages/koenig-lexical/src/components/ui/TenorPlugin.jsx
+++ b/packages/koenig-lexical/src/components/ui/GifPlugin.jsx
@@ -1,14 +1,15 @@
+import GifSelector from './GifSelector';
import KoenigComposerContext from '../../context/KoenigComposerContext.jsx';
import React from 'react';
-import TenorSelector from './TenorSelector';
import {DELETE_CARD_COMMAND} from '../../plugins/KoenigBehaviourPlugin.jsx';
-import {INSERT_FROM_TENOR_COMMAND} from '../../plugins/KoenigSelectorPlugin.jsx';
+import {INSERT_FROM_GIF_COMMAND} from '../../plugins/KoenigSelectorPlugin.jsx';
+import {getGifProviderConfig, useGif} from '../../utils/services/gif.js';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
-import {useTenor} from '../../utils/services/tenor.js';
-const TenorPlugin = ({nodeKey}) => {
+const GifPlugin = ({nodeKey}) => {
const {cardConfig} = React.useContext(KoenigComposerContext);
- const tenorHook = useTenor({config: cardConfig.tenor});
+ const providerConfig = getGifProviderConfig(cardConfig);
+ const gifHook = useGif({config: providerConfig});
const [editor] = useLexicalComposerContext();
React.useEffect(() => {
@@ -33,16 +34,17 @@ const TenorPlugin = ({nodeKey}) => {
};
const insertImageToNode = async (image) => {
- editor.dispatchCommand(INSERT_FROM_TENOR_COMMAND, image);
+ editor.dispatchCommand(INSERT_FROM_GIF_COMMAND, image);
};
return (
-
);
};
-export default TenorPlugin;
+export default GifPlugin;
diff --git a/packages/koenig-lexical/src/components/ui/TenorSelector.jsx b/packages/koenig-lexical/src/components/ui/GifSelector.jsx
similarity index 92%
rename from packages/koenig-lexical/src/components/ui/TenorSelector.jsx
rename to packages/koenig-lexical/src/components/ui/GifSelector.jsx
index 5a796ce3f9..ca24c08d11 100644
--- a/packages/koenig-lexical/src/components/ui/TenorSelector.jsx
+++ b/packages/koenig-lexical/src/components/ui/GifSelector.jsx
@@ -1,14 +1,14 @@
import React, {useEffect, useRef, useState} from 'react';
import SearchIcon from '../../assets/icons/kg-search.svg?react';
-import {Error} from './file-selectors/Tenor/Error';
-import {Gif} from './file-selectors/Tenor/Gif';
-import {Loader} from './file-selectors/Tenor/Loader';
+import {Error} from './file-selectors/Gif/Error';
+import {Gif} from './file-selectors/Gif/Gif';
+import {Loader} from './file-selectors/Gif/Loader';
// number of columns based on selector container width
const TWO_COLUMN_WIDTH = 540;
const THREE_COLUMN_WIDTH = 940;
-const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLoading, isLazyLoading, error, changeColumnCount, loadNextPage, gifs}) => {
+const GifSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLoading, isLazyLoading, error, changeColumnCount, loadNextPage, gifs, provider}) => {
const selectorRef = useRef(null);
const searchRef = useRef(null);
const [highlightedGif, setHighlightedGif] = useState(undefined);
@@ -138,7 +138,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
}
function moveToNextHorizontalGif(direction) {
- const highlightedElem = document.querySelector(`[data-tenor-index="${highlightedGif.index}"]`);
+ const highlightedElem = document.querySelector(`[data-gif-index="${highlightedGif.index}"]`);
const highlightedElemRect = highlightedElem.getBoundingClientRect();
let x;
@@ -155,9 +155,9 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
// we might hit spacing between gifs, keep moving up 5 px until we get a match
while (!foundGifElem) {
- let possibleMatch = document.elementFromPoint(x, y)?.closest('[data-tenor-index]');
+ let possibleMatch = document.elementFromPoint(x, y)?.closest('[data-gif-index]');
- if (possibleMatch?.dataset.tenorIndex !== undefined) {
+ if (possibleMatch?.dataset.gifIndex !== undefined) {
foundGifElem = possibleMatch;
break;
}
@@ -172,7 +172,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
}
if (foundGifElem) {
- setHighlightedGif(gifs[foundGifElem.dataset.tenorIndex]);
+ setHighlightedGif(gifs[foundGifElem.dataset.gifIndex]);
}
}
@@ -294,7 +294,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
e.stopPropagation()}
>
@@ -304,7 +304,7 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
@@ -330,11 +330,11 @@ const TenorSelector = ({onGifInsert, onClickOutside, updateSearch, columns, isLo
{!!isLoading && !error &&
}
- {!!error &&
}
+ {!!error &&
}
);
};
-export default TenorSelector;
+export default GifSelector;
diff --git a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx b/packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx
similarity index 72%
rename from packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx
rename to packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx
index b9b35d67d4..d122d4a61b 100644
--- a/packages/koenig-lexical/src/components/ui/TenorSelector.stories.jsx
+++ b/packages/koenig-lexical/src/components/ui/GifSelector.stories.jsx
@@ -1,10 +1,10 @@
-import TenorSelector from './TenorSelector';
-import {tenorConfig} from '../../../demo/utils/tenorConfig';
-import {useTenor} from '../../utils/services/tenor.js';
+import GifSelector from './GifSelector';
+import {getGifProviderConfig, useGif} from '../../utils/services/gif.js';
+import {tenorConfig} from '../../../demo/utils/gifConfig';
const story = {
- title: 'File Selectors/Tenor',
- component: TenorSelector,
+ title: 'File Selectors/Gif',
+ component: GifSelector,
parameters: {
status: {
type: 'Functional'
@@ -14,10 +14,10 @@ const story = {
export default story;
const Template = (args) => {
- const tenorHook = useTenor({config: tenorConfig});
+ const gifHook = useGif({config: getGifProviderConfig({tenor: tenorConfig})});
return (
-
+
);
};
diff --git a/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx
new file mode 100644
index 0000000000..b77144b271
--- /dev/null
+++ b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Error.jsx
@@ -0,0 +1,23 @@
+import {ERROR_TYPE} from '../../../../utils/services/gif.js';
+
+export function Error({error}) {
+ if (error === ERROR_TYPE.COMMON) {
+ return (
+
+ Uh-oh! Trouble reaching the GIF service, please check your connection
+
+ );
+ }
+
+ if (error === ERROR_TYPE.INVALID_API_KEY) {
+ return (
+
+ The GIF API key is not valid. Please check your configuration by following our
+ documentation here.
+
+ );
+ }
+ return (
+ {error}
+ );
+}
diff --git a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx
similarity index 95%
rename from packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx
rename to packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx
index 2acf6ab4f4..19a3c8a814 100644
--- a/packages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.jsx
+++ b/packages/koenig-lexical/src/components/ui/file-selectors/Gif/Gif.jsx
@@ -20,7 +20,7 @@ export function Gif({gif, onClick, highlightedGif = {}}) {