Skip to content

Commit

Permalink
feat: allow users to send gifs (via Tenor API) (#775)
Browse files Browse the repository at this point in the history
* feat: DocType 'Tenor Credentials' for API creds

* build: add '@tiptap/extension-image' extension

* chore: configure tiptap Image extension

* chore: types gen

* feat: GIF Picker

* fix: delete 'Tenor credential' DocType

* feat: added Tenor API key to Raven Settings

* chore: remove trailing whitespace

---------

Co-authored-by: Nikhil Kothari <nik.kothari22@live.com>
  • Loading branch information
yjane99 and nikkothari22 authored Mar 26, 2024
1 parent ffaa231 commit a53379d
Show file tree
Hide file tree
Showing 16 changed files with 336 additions and 10 deletions.
1 change: 1 addition & 0 deletions raven-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@radix-ui/themes": "^2.0.3",
"@tiptap/extension-code-block-lowlight": "^2.2.2",
"@tiptap/extension-highlight": "^2.2.2",
"@tiptap/extension-image": "^2.2.4",
"@tiptap/extension-link": "^2.2.2",
"@tiptap/extension-mention": "^2.2.2",
"@tiptap/extension-placeholder": "^2.2.2",
Expand Down
28 changes: 28 additions & 0 deletions raven-app/src/components/common/GIFPicker/GIFFeaturedResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useSWR } from "frappe-react-sdk"
import { TENOR_API_KEY, TENOR_CLIENT_KEY, TENOR_FEATURED_API_ENDPOINT_BASE } from "./GIFPicker"
import { GIFGallerySkeleton } from "./GIFGallerySkeleton"

export interface Props {
onSelect: (gif: Result) => void
}

const fetcher = (url: string) => fetch(url).then(res => res.json())

export const GIFFeaturedResults = ({ onSelect }: Props) => {

const { data: GIFS, isLoading } = useSWR<TenorResultObject>(`${TENOR_FEATURED_API_ENDPOINT_BASE}?&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}`, fetcher)

return (
<div className="overflow-y-auto h-[455px] w-[420px]">
{isLoading ? <GIFGallerySkeleton /> :
<div className="w-full columns-2 gap-2">
{GIFS && GIFS.results.map((gif, index) => (
<div key={index} className="animate-fadein" onClick={() => onSelect(gif)}>
<img className="h-full w-full rounded-sm bg-slate-6" src={gif.media_formats.tinygif.url} alt={gif.title} />
</div>
))}
</div>
}
</div>
)
}
48 changes: 48 additions & 0 deletions raven-app/src/components/common/GIFPicker/GIFGallerySkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Skeleton } from "../Skeleton"

export const GIFGallerySkeleton = () => {
return (
<div className="grid grid-cols-2 gap-2">
<div className="grid gap-2">
<Skeleton
style={{ height: '170px' }}
className="rounded-sm" />
<Skeleton
style={{ height: '220px' }}
className="rounded-sm" />
<Skeleton style={{ height: '120px' }}
className="rounded-sm" />
</div>
<div className="grid gap-2">
<Skeleton
style={{ height: '220px' }}
className="rounded-sm" />
<Skeleton style={{ height: '120px' }}
className="rounded-sm" />
<Skeleton
style={{ height: '170px' }}
className="rounded-sm" />
</div>
<div className="grid gap-2">
<Skeleton
style={{ height: '170px' }}
className="rounded-sm" />
<Skeleton
style={{ height: '220px' }}
className="rounded-sm" />
<Skeleton style={{ height: '120px' }}
className="rounded-sm" />
</div>
<div className="grid gap-2">
<Skeleton
style={{ height: '220px' }}
className="rounded-sm" />
<Skeleton
style={{ height: '170px' }}
className="rounded-sm" />
<Skeleton style={{ height: '120px' }}
className="rounded-sm" />
</div>
</div>
)
}
63 changes: 63 additions & 0 deletions raven-app/src/components/common/GIFPicker/GIFPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useDebounce } from "@/hooks/useDebounce"
import { Box, Flex, TextField } from "@radix-ui/themes"
import { useState } from "react"
import { BiSearch } from "react-icons/bi"
import { GIFSearchResults } from "./GIFSearchResults"
import { GIFFeaturedResults } from "./GIFFeaturedResults"

export const TENOR_SEARCH_API_ENDPOINT_BASE = `https://tenor.googleapis.com/v2/search`
export const TENOR_FEATURED_API_ENDPOINT_BASE = `https://tenor.googleapis.com/v2/featured`
// @ts-expect-error
export const TENOR_API_KEY = window.frappe?.boot.tenor_api_key
// @ts-expect-error
export const TENOR_CLIENT_KEY = import.meta.env.DEV ? `dev::${window.frappe?.boot.sitename}` : window.frappe?.boot.sitename

export interface GIFPickerProps {
onSelect: (gif: any) => void
}

/**
* GIF Picker component (in-house) to search and select GIFs
* @param onSelect - callback function to handle GIF selection
*/
export const GIFPicker = ({ onSelect }: GIFPickerProps) => {
// Get GIFs from Tenor API and display them
// show a search bar to search for GIFs
// on select, call onSelect with the gif URL

const [searchText, setSearchText] = useState("")
const debouncedText = useDebounce(searchText, 200)

return (
<Flex className="h-[550px] w-[450px] justify-center">
<Flex direction={'column'} gap='2' align='center' pt={'3'}>
<Box>
<TextField.Root className="w-[425px] mb-1">
<TextField.Slot>
<BiSearch />
</TextField.Slot>
<TextField.Input
onChange={(e) => setSearchText(e.target.value)}
value={searchText}
type='text'
placeholder='Search GIFs' />
</TextField.Root>
</Box>

{debouncedText.length >= 2 ? (
<GIFSearchResults query={debouncedText} onSelect={onSelect} />
) : (
<GIFFeaturedResults onSelect={onSelect} />
)}

<Box position={'fixed'} className="bottom-0 pb-2 bg-inherit">
<img
src="https://www.gstatic.com/tenor/web/attribution/PB_tenor_logo_blue_horizontal.png"
alt="Powered by Tenor"
width="100"
/>
</Box>
</Flex>
</Flex>
)
}
29 changes: 29 additions & 0 deletions raven-app/src/components/common/GIFPicker/GIFSearchResults.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useSWR } from "frappe-react-sdk"
import { TENOR_API_KEY, TENOR_CLIENT_KEY, TENOR_SEARCH_API_ENDPOINT_BASE } from "./GIFPicker"
import { GIFGallerySkeleton } from "./GIFGallerySkeleton"

export interface Props {
query: string
onSelect: (gif: Result) => void
}

const fetcher = (url: string) => fetch(url).then(res => res.json())

export const GIFSearchResults = ({ query, onSelect }: Props) => {

const { data: GIFS, isLoading } = useSWR<TenorResultObject>(`${TENOR_SEARCH_API_ENDPOINT_BASE}?q=${query}&key=${TENOR_API_KEY}&client_key=${TENOR_CLIENT_KEY}&limit=10`, fetcher)

return (
<div className="overflow-y-auto h-[455px] w-[420px]">
{isLoading ? <GIFGallerySkeleton /> :
<div className="w-full columns-2 gap-2">
{GIFS && GIFS.results.map((gif, index) => (
<div key={index} className="animate-fadein" onClick={() => onSelect(gif)}>
<img className="h-full w-full rounded-sm bg-slate-6" src={gif.media_formats.tinygif.url} alt={gif.title} />
</div>
))}
</div>
}
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { ToolbarFileProps } from './Tiptap'
import { Flex, IconButton, Inset, Popover, Separator } from '@radix-ui/themes'
import { Loader } from '@/components/common/Loader'
import { Suspense, lazy } from 'react'
import { HiOutlineGif } from "react-icons/hi2";
import { GIFPicker } from '@/components/common/GIFPicker/GIFPicker'


const EmojiPicker = lazy(() => import('@/components/common/EmojiPicker/EmojiPicker'))

Expand All @@ -31,6 +34,7 @@ export const RightToolbarButtons = ({ fileProps, ...sendProps }: RightToolbarBut
<Separator orientation='vertical' />
<Flex gap='3' align='center'>
<EmojiPickerButton />
<GIFPickerButton />
{fileProps && <FilePickerButton fileProps={fileProps} />}
<SendButton {...sendProps} />
</Flex>
Expand Down Expand Up @@ -111,6 +115,39 @@ const EmojiPickerButton = () => {
</Popover.Root>
}

const GIFPickerButton = () => {

const { editor } = useCurrentEditor()

if (!editor) {
return null
}

return <Popover.Root>
<Popover.Trigger>
<IconButton
size='1'
variant='ghost'
className={DEFAULT_BUTTON_STYLE}
title='Add GIF'
// disabled
aria-label={"add GIF"}>
<HiOutlineGif {...ICON_PROPS} />
</IconButton>
</Popover.Trigger>
<Popover.Content>
<Inset>
<Suspense fallback={<Loader />}>
{/* FIXME: 1. Handle 'HardBreak' coz it adds newline (empty); and if user doesn't write any text, then newline is added as text content.
2. Also if you write first & then add GIF there's no 'HardBreak'.
*/}
<GIFPicker onSelect={(gif) => editor.chain().focus().setImage({ src: gif.media_formats.gif.url }).setHardBreak().run()} />
</Suspense>
</Inset>
</Popover.Content>
</Popover.Root>
}

const FilePickerButton = ({ fileProps }: { fileProps: ToolbarFileProps }) => {
const { editor } = useCurrentEditor()
const fileButtonClicked = () => {
Expand Down Expand Up @@ -144,9 +181,13 @@ const SendButton = ({ sendMessage, messageSending, setContent }: {

const hasContent = editor.getText().trim().length > 0

console.log("Editor content: ", editor.getJSON())

const hasInlineImage = editor.getHTML().includes('img')

let html = ''
let json = {}
if (hasContent) {
if (hasContent || hasInlineImage) {
html = editor.getHTML()
json = editor.getJSON()
}
Expand Down
4 changes: 4 additions & 0 deletions raven-app/src/components/feature/chat/ChatInput/Tiptap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { Plugin } from 'prosemirror-state'
import { Box } from '@radix-ui/themes'
import { useSessionStickyState } from '@/hooks/useStickyState'
import { Message } from '../../../../../../types/Messaging/Message'
import Image from '@tiptap/extension-image'
const lowlight = createLowlight(common)

lowlight.register('html', html)
Expand Down Expand Up @@ -414,6 +415,9 @@ const Tiptap = ({ slotBefore, fileProps, onMessageSend, replyMessage, clearReply
CodeBlockLowlight.configure({
lowlight
}),
Image.configure({
inline: true,
}),
KeyboardHandler
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
border-top-right-radius: var(--radius-2);
}

.tiptap-editor.ProseMirror img {
width: 200px;
height: auto;
object-fit: content;

}

.tiptap-editor.replying.ProseMirror {
border-top-left-radius: 0;
border-top-right-radius: 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CustomUserMention } from './Mention'
import { CustomLink, LinkPreview } from './Link'
import { CustomItalic } from './Italic'
import { CustomUnderline } from './Underline'
import { Image } from '@tiptap/extension-image'
import { clsx } from 'clsx'
const lowlight = createLowlight(common)

Expand Down Expand Up @@ -79,7 +80,13 @@ export const TiptapRenderer = ({ message, user, isScrolling = false, isTruncated
CustomBold,
CustomUserMention,
CustomLink,
CustomItalic
CustomItalic,
Image.configure({
HTMLAttributes: {
class: 'w-full h-auto'
},
inline: true
}),
// TODO: Add channel mention
// CustomChannelMention
]
Expand Down
7 changes: 4 additions & 3 deletions raven-app/src/types/Raven/RavenUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

export interface RavenUser {
export interface RavenUser{
creation: string
name: string
modified: string
Expand All @@ -17,6 +17,7 @@ export interface RavenUser {
/** First Name : Data */
first_name?: string
/** User Image : Attach Image */
user_image?: string,
enabled: 0 | 1
user_image?: string
/** Enabled : Check */
enabled?: 0 | 1
}
48 changes: 48 additions & 0 deletions raven-app/src/types/RavenMessaging/RavenGIFPicker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
interface TenorResultObject {
results: Result[];
next: string;
}
interface Result {
id: string;
title: string;
media_formats: Mediaformats;
created: number;
content_description: string;
itemurl: string;
url: string;
tags: string[];
flags: any[];
hasaudio: boolean;
}
interface Mediaformats {
GIFFormatObject: GIFFormatObject;
mediumgif: GIFFormatObject;
tinywebp_transparent?: GIFFormatObject;
tinywebm: GIFFormatObject;
gif: GIFFormatObject;
tinygif: GIFFormatObject;
mp4: GIFFormatObject;
nanomp4: GIFFormatObject;
gifpreview: GIFFormatObject;
webm: GIFFormatObject;
nanowebp_transparent?: GIFFormatObject;
nanowebm: GIFFormatObject;
loopedmp4: GIFFormatObject;
tinygifpreview: GIFFormatObject;
nanowebppreview_transparent?: GIFFormatObject;
tinymp4: GIFFormatObject;
nanogif: GIFFormatObject;
webp_transparent?: GIFFormatObject;
webppreview_transparent?: GIFFormatObject;
tinywebppreview_transparent?: GIFFormatObject;
tinygif_transparent?: GIFFormatObject;
gif_transparent?: GIFFormatObject;
nanogif_transparent?: GIFFormatObject;
}
interface GIFFormatObject {
url: string;
duration: number;
preview: string;
dims: number[];
size: number;
}
2 changes: 1 addition & 1 deletion raven-app/src/types/RavenMessaging/RavenMessage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RavenMention } from './RavenMention'

export interface RavenMessage {
export interface RavenMessage{
creation: string
name: string
modified: string
Expand Down
5 changes: 5 additions & 0 deletions raven-app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,11 @@
resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.2.2.tgz#209f4582882d16893115514f38dd1c2ae2436ec7"
integrity sha512-5hun56M9elO6slOoDH03q2of06KB1rX8MLvfiKpfAvjbhmuQJav20fz2MQ2lCunek0D8mUIySwhfMvBrTcd90A==

"@tiptap/extension-image@^2.2.4":
version "2.2.4"
resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.2.4.tgz#55c267e9ff77e2bf3c514896331ad91d18da08c3"
integrity sha512-xOnqZpnP/fAfmK5AKmXplVQdXBtY5AoZ9B+qllH129aLABaDRzl3e14ZRHC8ahQawOmCe6AOCCXYUBXDOlY5Jg==

"@tiptap/extension-italic@^2.2.2":
version "2.2.2"
resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.2.2.tgz#fe8ac8abd39f723f4885502341e3d088ed003e7a"
Expand Down
Loading

0 comments on commit a53379d

Please sign in to comment.