Skip to content
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: allow users to send gifs (via Tenor API) #775

Merged
merged 9 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading