mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/composer: add gif picker
This commit is contained in:
parent
dee7e5c72d
commit
d55952a1b6
10 changed files with 401 additions and 37 deletions
BIN
web/public/images/powered-by-giphy.png
Normal file
BIN
web/public/images/powered-by-giphy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
34
web/public/images/powered-by-tenor.svg
Normal file
34
web/public/images/powered-by-tenor.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
|
@ -27,9 +27,11 @@ export const codeBlockStyles = [
|
|||
"tokyonight-storm", "trac", "vim", "vs", "vulcan", "witchhazel", "xcode-dark", "xcode",
|
||||
] as const
|
||||
export const mapProviders = ["leaflet", "google", "none"] as const
|
||||
export const gifProviders = ["giphy", "tenor"] as const
|
||||
|
||||
export type CodeBlockStyle = typeof codeBlockStyles[number]
|
||||
export type MapProvider = typeof mapProviders[number]
|
||||
export type GIFProvider = typeof gifProviders[number]
|
||||
|
||||
/* eslint-disable max-len */
|
||||
export const preferences = {
|
||||
|
@ -119,6 +121,20 @@ export const preferences = {
|
|||
allowedContexts: anyContext,
|
||||
defaultValue: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
}),
|
||||
gif_provider: new Preference<GIFProvider>({
|
||||
displayName: "GIF provider",
|
||||
description: "The service to use to search for GIFs",
|
||||
allowedValues: gifProviders,
|
||||
allowedContexts: anyContext,
|
||||
defaultValue: "giphy",
|
||||
}),
|
||||
// TODO implement
|
||||
// reupload_gifs: new Preference<boolean>({
|
||||
// displayName: "Reupload GIFs",
|
||||
// description: "Should GIFs be reuploaded to your server's media repo instead of using the proxy?",
|
||||
// allowedContexts: anyContext,
|
||||
// defaultValue: false,
|
||||
// }),
|
||||
custom_notification_sound: new Preference<ContentURI>({
|
||||
displayName: "Custom notification sound",
|
||||
description: "The mxc:// URI to a custom notification sound.",
|
||||
|
|
1
web/src/icons/gif.svg
Normal file
1
web/src/icons/gif.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v560q0 33-23.5 56.5T760-120H200Zm0-80h560v-560H200v560Zm240-160h60v-240h-60v240Zm-160 0h80q17 0 28.5-11.5T400-400v-80h-60v60h-40v-120h100v-20q0-17-11.5-28.5T360-600h-80q-17 0-28.5 11.5T240-560v160q0 17 11.5 28.5T280-360Zm280 0h60v-80h80v-60h-80v-40h120v-60H560v240ZM200-200v-560 560Z"/></svg>
|
After Width: | Height: | Size: 496 B |
1
web/src/icons/sticker.svg
Normal file
1
web/src/icons/sticker.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#5f6368"><path d="M5.5,2C3.56,2 2,3.56 2,5.5V18.5C2,20.44 3.56,22 5.5,22H16L22,16V5.5C22,3.56 20.44,2 18.5,2H5.5M5.75,4H18.25A1.75,1.75 0 0,1 20,5.75V15H18.5C16.56,15 15,16.56 15,18.5V20H5.75A1.75,1.75 0 0,1 4,18.25V5.75A1.75,1.75 0 0,1 5.75,4M14.44,6.77C14.28,6.77 14.12,6.79 13.97,6.83C13.03,7.09 12.5,8.05 12.74,9C12.79,9.15 12.86,9.3 12.95,9.44L16.18,8.56C16.18,8.39 16.16,8.22 16.12,8.05C15.91,7.3 15.22,6.77 14.44,6.77M8.17,8.5C8,8.5 7.85,8.5 7.7,8.55C6.77,8.81 6.22,9.77 6.47,10.7C6.5,10.86 6.59,11 6.68,11.16L9.91,10.28C9.91,10.11 9.89,9.94 9.85,9.78C9.64,9 8.95,8.5 8.17,8.5M16.72,11.26L7.59,13.77C8.91,15.3 11,15.94 12.95,15.41C14.9,14.87 16.36,13.25 16.72,11.26Z" /></svg>
|
After Width: | Height: | Size: 750 B |
|
@ -31,6 +31,7 @@ import { escapeMarkdown } from "@/util/markdown.ts"
|
|||
import useEvent from "@/util/useEvent.ts"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import EmojiPicker from "../emojipicker/EmojiPicker.tsx"
|
||||
import GIFPicker from "../emojipicker/GIFPicker.tsx"
|
||||
import { keyToString } from "../keybindings.ts"
|
||||
import { LeafletPicker } from "../maps/async.tsx"
|
||||
import { ModalContext } from "../modal/Modal.tsx"
|
||||
|
@ -42,6 +43,7 @@ import { charToAutocompleteType, emojiQueryRegex, getAutocompleter } from "./get
|
|||
import AttachIcon from "@/icons/attach.svg?react"
|
||||
import CloseIcon from "@/icons/close.svg?react"
|
||||
import EmojiIcon from "@/icons/emoji-categories/smileys-emotion.svg?react"
|
||||
import GIFIcon from "@/icons/gif.svg?react"
|
||||
import LocationIcon from "@/icons/location.svg?react"
|
||||
import SendIcon from "@/icons/send.svg?react"
|
||||
import "./MessageComposer.css"
|
||||
|
@ -400,19 +402,26 @@ const MessageComposer = () => {
|
|||
evt.stopPropagation()
|
||||
roomCtx.setEditing(null)
|
||||
}, [roomCtx])
|
||||
const onSelectEmoji = useEvent((emoji: PartialEmoji) => {
|
||||
setState({
|
||||
text: state.text.slice(0, textInput.current?.selectionStart ?? 0)
|
||||
+ emojiToMarkdown(emoji)
|
||||
+ state.text.slice(textInput.current?.selectionEnd ?? 0),
|
||||
})
|
||||
})
|
||||
const openEmojiPicker = useEvent(() => {
|
||||
openModal({
|
||||
content: <EmojiPicker
|
||||
style={{ bottom: (composerRef.current?.clientHeight ?? 32) + 2, right: "1rem" }}
|
||||
room={roomCtx.store}
|
||||
onSelect={onSelectEmoji}
|
||||
onSelect={(emoji: PartialEmoji) => setState({
|
||||
text: state.text.slice(0, textInput.current?.selectionStart ?? 0)
|
||||
+ emojiToMarkdown(emoji)
|
||||
+ state.text.slice(textInput.current?.selectionEnd ?? 0),
|
||||
})}
|
||||
/>,
|
||||
onClose: () => textInput.current?.focus(),
|
||||
})
|
||||
})
|
||||
const openGIFPicker = useEvent(() => {
|
||||
openModal({
|
||||
content: <GIFPicker
|
||||
style={{ bottom: (composerRef.current?.clientHeight ?? 32) + 2, right: "1rem" }}
|
||||
room={roomCtx.store}
|
||||
onSelect={media => setState({ media })}
|
||||
/>,
|
||||
onClose: () => textInput.current?.focus(),
|
||||
})
|
||||
|
@ -476,20 +485,22 @@ const MessageComposer = () => {
|
|||
placeholder="Send a message"
|
||||
id="message-composer"
|
||||
/>
|
||||
<button onClick={openEmojiPicker}><EmojiIcon/></button>
|
||||
<button onClick={openEmojiPicker} title="Add emoji"><EmojiIcon/></button>
|
||||
<button onClick={openGIFPicker} title="Add gif attachment"><GIFIcon/></button>
|
||||
<button
|
||||
onClick={openLocationPicker}
|
||||
disabled={!!locationDisabledTitle}
|
||||
title={locationDisabledTitle}
|
||||
title={locationDisabledTitle ?? "Add location"}
|
||||
><LocationIcon/></button>
|
||||
<button
|
||||
onClick={openFilePicker}
|
||||
disabled={!!mediaDisabledTitle}
|
||||
title={mediaDisabledTitle}
|
||||
title={mediaDisabledTitle ?? "Add file attachment"}
|
||||
><AttachIcon/></button>
|
||||
<button
|
||||
onClick={sendMessage}
|
||||
disabled={(!state.text && !state.media && !state.location) || loadingMedia}
|
||||
title="Send message"
|
||||
><SendIcon/></button>
|
||||
<input ref={fileInput} onChange={onAttachFile} type="file" value=""/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
div.emoji-picker {
|
||||
div.emoji-picker, div.gif-picker {
|
||||
position: fixed;
|
||||
background-color: var(--background-color);
|
||||
width: 22rem;
|
||||
|
@ -9,30 +9,7 @@ div.emoji-picker {
|
|||
flex-direction: column;
|
||||
box-shadow: 0 0 1rem var(--modal-box-shadow-color);
|
||||
|
||||
div.emoji-category-bar {
|
||||
/*height: 2.5rem;*/
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding-top: .5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
> button {
|
||||
padding-top: .25rem;
|
||||
width: 2.125rem;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
&:hover {
|
||||
border-bottom: 2px solid var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.emoji-search {
|
||||
div.emoji-search, div.gif-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: .5rem;
|
||||
|
@ -56,6 +33,68 @@ div.emoji-picker {
|
|||
border-top-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.gif-picker {
|
||||
width: 32rem;
|
||||
|
||||
> div.gif-list {
|
||||
overflow-y: auto;
|
||||
padding: 0 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> div.gif-entry {
|
||||
cursor: var(--clickable-cursor);
|
||||
max-width: 10rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--button-hover-color);
|
||||
}
|
||||
|
||||
> img {
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
padding: .5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.powered-by-footer {
|
||||
margin-top: auto;
|
||||
margin-bottom: .5rem;
|
||||
> img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.emoji-picker {
|
||||
div.emoji-category-bar {
|
||||
/*height: 2.5rem;*/
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
padding-top: .5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
> button {
|
||||
padding-top: .25rem;
|
||||
width: 2.125rem;
|
||||
height: 2.5rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
&:hover {
|
||||
border-bottom: 2px solid var(--primary-color-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.emoji-list {
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -59,7 +59,7 @@ interface EmojiPickerProps {
|
|||
selected?: string[]
|
||||
}
|
||||
|
||||
export const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSelect }: EmojiPickerProps) => {
|
||||
const EmojiPicker = ({ style, selected, onSelect, room, allowFreeform, closeOnSelect }: EmojiPickerProps) => {
|
||||
const client = use(ClientContext)!
|
||||
const [query, setQuery] = useState("")
|
||||
const [previewEmoji, setPreviewEmoji] = useState<Emoji>()
|
||||
|
|
135
web/src/ui/emojipicker/GIFPicker.tsx
Normal file
135
web/src/ui/emojipicker/GIFPicker.tsx
Normal file
|
@ -0,0 +1,135 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import React, { CSSProperties, use, useCallback, useEffect, useState } from "react"
|
||||
import { RoomStateStore, usePreference } from "@/api/statestore"
|
||||
import { MediaMessageEventContent } from "@/api/types"
|
||||
import ClientContext from "../ClientContext.ts"
|
||||
import { ModalCloseContext } from "../modal/Modal.tsx"
|
||||
import { GIF, getTrendingGIFs, searchGIF } from "./gifsource.ts"
|
||||
import CloseIcon from "@/icons/close.svg?react"
|
||||
import SearchIcon from "@/icons/search.svg?react"
|
||||
|
||||
interface GIFPickerProps {
|
||||
style: CSSProperties
|
||||
onSelect: (media: MediaMessageEventContent) => void
|
||||
room: RoomStateStore
|
||||
}
|
||||
|
||||
const trendingCache = new Map<string, GIF[]>()
|
||||
|
||||
const GIFPicker = ({ style, onSelect, room }: GIFPickerProps) => {
|
||||
const [query, setQuery] = useState("")
|
||||
const [results, setResults] = useState<GIF[]>([])
|
||||
const [error, setError] = useState<unknown>()
|
||||
const close = use(ModalCloseContext)
|
||||
const clearQuery = useCallback(() => setQuery(""), [])
|
||||
const onChangeQuery = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => setQuery(evt.target.value), [])
|
||||
const client = use(ClientContext)!
|
||||
const provider = usePreference(client.store, room, "gif_provider")
|
||||
const providerName = provider.slice(0, 1).toUpperCase() + provider.slice(1)
|
||||
// const reuploadGIFs = room.preferences.reupload_gifs
|
||||
const onSelectGIF = useCallback((evt: React.MouseEvent<HTMLDivElement>) => {
|
||||
const idx = evt.currentTarget.getAttribute("data-gif-index")
|
||||
if (!idx) {
|
||||
return
|
||||
}
|
||||
const gif = results[+idx]
|
||||
// if (reuploadGIFs) {
|
||||
// // TODO
|
||||
// }
|
||||
onSelect({
|
||||
msgtype: "m.image",
|
||||
body: gif.filename,
|
||||
info: {
|
||||
mimetype: "image/webp",
|
||||
size: gif.size,
|
||||
w: gif.width,
|
||||
h: gif.height,
|
||||
},
|
||||
url: gif.proxied_mxc,
|
||||
})
|
||||
close()
|
||||
}, [onSelect, close, results])
|
||||
useEffect(() => {
|
||||
if (!query) {
|
||||
if (trendingCache.has(provider)) {
|
||||
setResults(trendingCache.get(provider)!)
|
||||
return
|
||||
} else {
|
||||
const abort = new AbortController()
|
||||
getTrendingGIFs(provider).then(
|
||||
res => {
|
||||
trendingCache.set(provider, res)
|
||||
if (!abort.signal.aborted) {
|
||||
setResults(res)
|
||||
}
|
||||
},
|
||||
err => !abort.signal.aborted && setError(err),
|
||||
)
|
||||
return () => abort.abort()
|
||||
}
|
||||
}
|
||||
const abort = new AbortController()
|
||||
const timeout = setTimeout(() => {
|
||||
searchGIF(provider, query, abort.signal).then(
|
||||
setResults,
|
||||
err => !abort.signal.aborted && setError(err),
|
||||
)
|
||||
}, 500)
|
||||
return () => {
|
||||
clearTimeout(timeout)
|
||||
abort.abort()
|
||||
}
|
||||
}, [query, provider])
|
||||
let poweredBySrc: string | undefined
|
||||
if (provider === "giphy") {
|
||||
poweredBySrc = "images/powered-by-giphy.png"
|
||||
} else if (provider === "tenor") {
|
||||
poweredBySrc = "images/powered-by-tenor.svg"
|
||||
}
|
||||
return <div className="gif-picker" style={style}>
|
||||
<div className="gif-search">
|
||||
<input
|
||||
autoFocus
|
||||
onChange={onChangeQuery}
|
||||
value={query}
|
||||
type="search"
|
||||
placeholder={`Search ${providerName}`}
|
||||
/>
|
||||
<button onClick={clearQuery} disabled={query === ""}>
|
||||
{query !== "" ? <CloseIcon/> : <SearchIcon/>}
|
||||
</button>
|
||||
</div>
|
||||
{error ? <div className="gif-error">
|
||||
{`${error}`}
|
||||
</div> : null}
|
||||
<div className="gif-list">
|
||||
{results.map((gif, idx) => <div
|
||||
className="gif-entry"
|
||||
key={gif.key}
|
||||
data-gif-index={idx}
|
||||
onClick={onSelectGIF}
|
||||
>
|
||||
<img loading="lazy" src={gif.https_url} alt={gif.alt_text}/>
|
||||
</div>)}
|
||||
{poweredBySrc && <div className="powered-by-footer">
|
||||
<img src={poweredBySrc} alt={`Powered by ${providerName}`}/>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default GIFPicker
|
127
web/src/ui/emojipicker/gifsource.ts
Normal file
127
web/src/ui/emojipicker/gifsource.ts
Normal file
|
@ -0,0 +1,127 @@
|
|||
// gomuks - A Matrix client written in Go.
|
||||
// Copyright (C) 2024 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { ContentURI } from "@/api/types"
|
||||
import { GIFProvider } from "@/api/types/preferences"
|
||||
import { GIPHY_API_KEY, TENOR_API_KEY } from "@/util/keys.ts"
|
||||
|
||||
export interface GIF {
|
||||
key: string
|
||||
filename: string
|
||||
title: string
|
||||
alt_text: string
|
||||
proxied_mxc: ContentURI
|
||||
https_url: string
|
||||
width: number
|
||||
height: number
|
||||
size: number
|
||||
}
|
||||
|
||||
function mapGiphyResults(results: unknown[]): GIF[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return results.map((entry: any): GIF => ({
|
||||
key: entry.id,
|
||||
filename: `${entry.slug}.webp`,
|
||||
title: entry.title,
|
||||
alt_text: entry.alt_text,
|
||||
proxied_mxc: `mxc://giphy.mau.dev/${entry.id}`,
|
||||
https_url: entry.images.original.webp,
|
||||
size: entry.images.original.webp_size,
|
||||
width: entry.images.original.width,
|
||||
height: entry.images.original.height,
|
||||
}))
|
||||
}
|
||||
|
||||
const tenorMediaURLRegex = /https:\/\/media\.tenor\.com\/([A-Za-z0-9_-]+)\/.+/
|
||||
|
||||
function mapTenorResults(results: unknown[]): GIF[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return results.map((entry: any): GIF | undefined => {
|
||||
const id = tenorMediaURLRegex.exec(entry.media_formats.webp.url)?.[1]
|
||||
if (!id) {
|
||||
return
|
||||
}
|
||||
return {
|
||||
key: entry.id,
|
||||
filename: `${entry.id}.webp`,
|
||||
title: entry.title,
|
||||
alt_text: entry.alt_text,
|
||||
proxied_mxc: `mxc://tenor.mau.dev/${id}`,
|
||||
https_url: entry.media_formats.webp.url,
|
||||
size: entry.media_formats.webp.size,
|
||||
width: entry.media_formats.webp.dims[0],
|
||||
height: entry.media_formats.webp.dims[1],
|
||||
}
|
||||
}).filter((entry: GIF | undefined): entry is GIF => !!entry)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async function doRequest(url: URL, signal?: AbortSignal): Promise<any> {
|
||||
const resp = await fetch(url, { signal })
|
||||
if (resp.status !== 200) {
|
||||
throw new Error(`HTTP ${resp.status}: ${await resp.text()}`)
|
||||
}
|
||||
return await resp.json()
|
||||
}
|
||||
|
||||
async function searchGiphy(signal: AbortSignal, query: string): Promise<GIF[]> {
|
||||
const url = new URL("https://api.giphy.com/v1/gifs/search")
|
||||
url.searchParams.set("api_key", GIPHY_API_KEY)
|
||||
url.searchParams.set("q", query)
|
||||
url.searchParams.set("limit", "50")
|
||||
return mapGiphyResults((await doRequest(url, signal)).data)
|
||||
}
|
||||
|
||||
async function searchTenor(signal: AbortSignal, query: string): Promise<GIF[]> {
|
||||
const url = new URL("https://tenor.googleapis.com/v2/search")
|
||||
url.searchParams.set("key", TENOR_API_KEY)
|
||||
url.searchParams.set("media_filter", "webp")
|
||||
url.searchParams.set("q", query)
|
||||
url.searchParams.set("limit", "50")
|
||||
return mapTenorResults((await doRequest(url, signal)).results)
|
||||
}
|
||||
|
||||
async function getGiphyTrending(): Promise<GIF[]> {
|
||||
const url = new URL("https://api.giphy.com/v1/gifs/trending")
|
||||
url.searchParams.set("api_key", GIPHY_API_KEY)
|
||||
url.searchParams.set("limit", "50")
|
||||
return mapGiphyResults((await doRequest(url)).data)
|
||||
}
|
||||
|
||||
async function getTenorTrending(): Promise<GIF[]> {
|
||||
const url = new URL("https://tenor.googleapis.com/v2/featured")
|
||||
url.searchParams.set("key", TENOR_API_KEY)
|
||||
url.searchParams.set("media_filter", "webp")
|
||||
url.searchParams.set("limit", "50")
|
||||
return mapTenorResults((await doRequest(url)).results)
|
||||
}
|
||||
|
||||
const searchFuncs = {
|
||||
giphy: searchGiphy,
|
||||
tenor: searchTenor,
|
||||
}
|
||||
|
||||
const trendingFuncs = {
|
||||
giphy: getGiphyTrending,
|
||||
tenor: getTenorTrending,
|
||||
}
|
||||
|
||||
export async function searchGIF(provider: GIFProvider, query: string, signal: AbortSignal): Promise<GIF[]> {
|
||||
return searchFuncs[provider](signal, query)
|
||||
}
|
||||
|
||||
export async function getTrendingGIFs(provider: GIFProvider): Promise<GIF[]> {
|
||||
return trendingFuncs[provider]()
|
||||
}
|
Loading…
Add table
Reference in a new issue