-
Notifications
You must be signed in to change notification settings - Fork 336
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Matt Krick <matt.krick@gmail.com>
- Loading branch information
Showing
13 changed files
with
364 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
40 changes: 40 additions & 0 deletions
40
packages/client/tiptap/extensions/imageUpload/ImageSelectorSearchTab.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import {useRef} from 'react' | ||
import {Button} from '../../../ui/Button/Button' | ||
|
||
interface Props { | ||
setImageURL: (url: string) => void | ||
} | ||
|
||
export const ImageSelectorSearchTab = (props: Props) => { | ||
const {setImageURL} = props | ||
const ref = useRef<HTMLInputElement>(null) | ||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { | ||
e.preventDefault() | ||
const url = ref.current?.value | ||
if (!url) return | ||
setImageURL(url!) | ||
} | ||
const service = window.__ACTION__.GIF_PROVIDER | ||
// Per attribution spec, the exact wording is required | ||
// https://developers.google.com/tenor/guides/attribution | ||
const placeholder = service === 'tenor' ? 'Search Tenor' : 'Search Gifs' | ||
|
||
return ( | ||
<form | ||
className='flex w-full min-w-44 flex-col items-center justify-center space-y-3 rounded-md bg-slate-100 p-2' | ||
onSubmit={onSubmit} | ||
> | ||
<input | ||
autoComplete='off' | ||
autoFocus | ||
placeholder={placeholder} | ||
type='url' | ||
className='w-full outline-none focus:ring-2' | ||
ref={ref} | ||
/> | ||
<Button variant='outline' shape='pill' className='w-full' type='submit'> | ||
Search image | ||
</Button> | ||
</form> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import {TenorManager} from '../../../utils/TenorManager' | ||
import {QueryResolvers} from '../resolverTypes' | ||
|
||
export interface SSORelayState { | ||
isInvited?: boolean | ||
metadataURL?: string | ||
} | ||
|
||
const searchGifs: QueryResolvers['searchGifs'] = async (_source, {query, first, after}) => { | ||
const service = process.env.GIF_PROVIDER || 'tenor' | ||
if (service === 'tenor') { | ||
const manager = new TenorManager() | ||
const request = | ||
query === '' | ||
? manager.featured({limit: first, pos: after}) | ||
: manager.search({query, limit: first, pos: after}) | ||
const res = await request | ||
if (res instanceof Error) { | ||
throw res | ||
} | ||
const {next, results} = res | ||
const nodes = results.map((result) => { | ||
const {content_description: description, tags, id, media_formats} = result | ||
const {nanowebp_transparent, tinywebp_transparent, webp_transparent} = media_formats | ||
return { | ||
id, | ||
description, | ||
tags, | ||
urlOriginal: webp_transparent.url, | ||
urlTiny: tinywebp_transparent.url, | ||
urlNano: nanowebp_transparent.url | ||
} | ||
}) | ||
const edges = nodes.map((node, idx) => ({ | ||
node, | ||
cursor: idx === nodes.length - 1 ? next : null | ||
})) | ||
|
||
return { | ||
pageInfo: { | ||
hasNextPage: !!next, | ||
hasPreviousPage: false, | ||
startCursor: null, | ||
endCursor: next | ||
}, | ||
edges | ||
} | ||
} | ||
console.log(`${service} NOT IMPLEMENTED!`) | ||
return { | ||
pageInfo: { | ||
hasNextPage: false, | ||
hasPreviousPage: false, | ||
startCursor: null, | ||
endCursor: null | ||
}, | ||
edges: [] | ||
} | ||
} | ||
|
||
export default searchGifs |
29 changes: 29 additions & 0 deletions
29
packages/server/graphql/public/typeDefs/GifResponse.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
""" | ||
A response with info about a gif | ||
""" | ||
type GifResponse { | ||
""" | ||
The ID of the gif | ||
""" | ||
id: ID! | ||
|
||
""" | ||
A description of the gif | ||
""" | ||
description: String! | ||
|
||
""" | ||
A list of tags describing the gif | ||
""" | ||
tags: [String!]! | ||
|
||
""" | ||
url | ||
""" | ||
url( | ||
""" | ||
The size of the gif | ||
""" | ||
size: ImageSize! | ||
): String! | ||
} |
14 changes: 14 additions & 0 deletions
14
packages/server/graphql/public/typeDefs/GifResponseConnection.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
""" | ||
A connection to list the returned gifs | ||
""" | ||
type GifResponseConnection { | ||
""" | ||
Page info with cursors as strings | ||
""" | ||
pageInfo: PageInfo | ||
|
||
""" | ||
A list of edges. | ||
""" | ||
edges: [GifResponseEdge!]! | ||
} |
10 changes: 10 additions & 0 deletions
10
packages/server/graphql/public/typeDefs/GifResponseEdge.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
""" | ||
An edge in a connection. | ||
""" | ||
type GifResponseEdge { | ||
""" | ||
The item at the end of the edge | ||
""" | ||
node: GifResponse! | ||
cursor: String | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
""" | ||
The size of an image | ||
""" | ||
enum ImageSize { | ||
""" | ||
Less than 90px tall | ||
""" | ||
nano | ||
""" | ||
Less than 220px tall | ||
""" | ||
tiny | ||
""" | ||
full size | ||
""" | ||
original | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import {GifResponseResolvers} from '../resolverTypes' | ||
|
||
export type GifResponseSource = { | ||
id: string | ||
description: string | ||
tags: string[] | ||
urlOriginal: string | ||
urlTiny: string | ||
urlNano: string | ||
} | ||
|
||
const GifResponse: GifResponseResolvers = { | ||
url: (source, {size}) => { | ||
const {urlNano, urlOriginal, urlTiny} = source | ||
if (size === 'nano') return urlNano | ||
if (size === 'tiny') return urlTiny | ||
return urlOriginal | ||
} | ||
} | ||
|
||
export default GifResponse |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
const MAX_REQUEST_TIME = 5000 | ||
|
||
interface TenorResponse { | ||
results: ResponseObject[] | ||
next: string | ||
} | ||
|
||
interface ResponseObject { | ||
created: number // Unix timestamp representing when this post was created | ||
hasaudio: boolean // Indicates if the post contains audio (only video formats support audio) | ||
id: string // Tenor result identifier | ||
media_formats: Record<MediaFilter, MediaObject> // Dictionary with content format as the key and MediaObject as the value | ||
tags: string[] // Array of tags for the post | ||
title: string // Title of the post | ||
content_description: string // Textual description of the content | ||
itemurl: string // Full URL to view the post on tenor.com | ||
hascaption: boolean // Indicates if the post contains captions | ||
flags: string // Comma-separated list to describe content properties (e.g., sticker, static, audio) | ||
bg_color: string // Most common background pixel color of the content | ||
url: string // Short URL to view the post on tenor.com | ||
} | ||
|
||
interface MediaObject { | ||
url: string // URL to the media content | ||
duration?: number // Duration of the media (if applicable) | ||
preview?: string // URL to the preview image (if applicable) | ||
dims?: [number, number] // Dimensions of the media (width, height) | ||
size?: number // Size of the media file in bytes | ||
} | ||
|
||
const mediaFilter = [ | ||
'webp', | ||
'webp_transparent', | ||
'tinywebp_transparent', | ||
'nanowebp_transparent' | ||
] as const | ||
|
||
type MediaFilter = (typeof mediaFilter)[number] | ||
export class TenorManager { | ||
apiKey: string | ||
clientKey: string | ||
constructor() { | ||
const {HOST, TENOR_SECRET} = process.env | ||
if (!TENOR_SECRET) { | ||
throw new Error('Missing ENV Var: TENOR_SECRET') | ||
} | ||
this.apiKey = TENOR_SECRET | ||
this.clientKey = HOST! | ||
} | ||
private fetchWithTimeout = async (url: string, options: RequestInit) => { | ||
const controller = new AbortController() | ||
const {signal} = controller | ||
const timeout = setTimeout(() => { | ||
controller.abort() | ||
}, MAX_REQUEST_TIME) | ||
try { | ||
const res = await fetch(url, {...options, signal}) | ||
clearTimeout(timeout) | ||
return res | ||
} catch (e) { | ||
clearTimeout(timeout) | ||
return new Error('Tenor is not responding') | ||
} | ||
} | ||
private async get<T>(url: string): Promise<T | Error> { | ||
const res = await this.fetchWithTimeout(url, {}) | ||
if (res instanceof Error) return res | ||
if (res.status !== 200) { | ||
return new Error(`${res.status}: ${res.statusText}`) | ||
} | ||
const resJSON = await res.json() | ||
return resJSON as T | ||
} | ||
|
||
async featured(opts: {limit: number; pos?: string | null; country?: string; locale?: string}) { | ||
const {limit, country, locale, pos} = opts | ||
const url = new URL(`https://tenor.googleapis.com/v2/featured`) | ||
const searchParams = { | ||
key: this.apiKey, | ||
client_key: this.clientKey, | ||
media_filter: mediaFilter.join(','), | ||
limit, | ||
pos, | ||
country, | ||
locale | ||
} | ||
Object.entries(searchParams).forEach(([key, value]) => { | ||
// filters out country, locale | ||
if (!value) return | ||
url.searchParams.append(key, value as string) | ||
}) | ||
return await this.get<TenorResponse>(url.toString()) | ||
} | ||
|
||
async search(opts: { | ||
query: string | ||
limit: number | ||
pos?: string | null | ||
country?: string | ||
locale?: string | ||
}) { | ||
const {query, limit, country, locale, pos} = opts | ||
const url = new URL(`https://tenor.googleapis.com/v2/search`) | ||
const searchParams = { | ||
key: this.apiKey, | ||
client_key: this.clientKey, | ||
media_filter: mediaFilter.join(','), | ||
limit, | ||
pos, | ||
country, | ||
locale, | ||
query | ||
} | ||
Object.entries(searchParams).forEach(([key, value]) => { | ||
// filters out country, locale | ||
if (!value) return | ||
url.searchParams.append(key, value as string) | ||
}) | ||
return await this.get<TenorResponse>(url.toString()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.