Skip to content

Commit

Permalink
feat: gifabol (tenor search) (#10735)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored Jan 28, 2025
1 parent f3a911d commit d893258
Show file tree
Hide file tree
Showing 22 changed files with 576 additions and 87 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,7 @@ PGADMIN_DEFAULT_PASSWORD='admin'
# GLOBAL_BANNER_TEXT='UNCLASSIFIED CUI (IL4)'
# GLOBAL_BANNER_BG_COLOR='#007A33'
# GLOBAL_BANNER_COLOR='#FFFFFF'

# gifabol | tenor | '' to hide gif selection tab
# GIF_PROVIDER=tenor
# TENOR_SECRET=''
1 change: 1 addition & 0 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource",
"GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource",
"GenerateRetroSummariesSuccess": "./types/GenerateRetroSummariesSuccess#GenerateRetroSummariesSuccessSource",
"GifResponse": "./types/GifResponse#GifResponseSource",
"GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth",
"GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource",
"IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider",
Expand Down
3 changes: 2 additions & 1 deletion packages/client/hooks/useTipTapReflectionEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {mentionConfig, serverTipTapExtensions} from '../shared/tiptap/serverTipT
import ImageBlock from '../tiptap/extensions/imageBlock/ImageBlock'
import {ImageUpload} from '../tiptap/extensions/imageUpload/ImageUpload'
import {SlashCommand} from '../tiptap/extensions/slashCommand/SlashCommand'
import {ElementWidth} from '../types/constEnums'
import {tiptapEmojiConfig} from '../utils/tiptapEmojiConfig'
import {tiptapMentionConfig} from '../utils/tiptapMentionConfig'

Expand Down Expand Up @@ -69,7 +70,7 @@ export const useTipTapReflectionEditor = (
'To-do list': false
}),
Focus,
ImageUpload,
ImageUpload.configure({editorWidth: ElementWidth.REFLECTION_CARD}),
ImageBlock,
LoomExtension,
Placeholder.configure({
Expand Down
22 changes: 12 additions & 10 deletions packages/client/styles/theme/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
text-decoration: none;
}

button {
@apply p-0;
}

button,
input,
select,
Expand Down Expand Up @@ -170,6 +174,8 @@

.ProseMirror {
width: 100%;
/* Gap cursor is 2px above the top of an element */
padding-top: 2px;
blockquote {
border-left: 3px solid theme('colors.slate.500');
margin: 1.5rem 0;
Expand Down Expand Up @@ -233,17 +239,13 @@
}
}
.node-imageBlock {
& img {
@apply overflow-hidden rounded-xl border-2 border-transparent;
}

&:hover img {
@apply border-2 border-slate-100;
@apply relative;
&.has-focus > div::after {
content: '';
@apply absolute inset-0 h-full w-full bg-[#2383e247];
}

&:has(.is-active) img,
&.has-focus img {
@apply border-2 border-slate-800;
& img {
@apply overflow-hidden rounded-md border-2 border-transparent;
}
}
}
Expand Down
62 changes: 37 additions & 25 deletions packages/client/tiptap/extensions/imageUpload/ImageSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {useState} from 'react'
import Tab from '../../../components/Tab/Tab'
import Tabs from '../../../components/Tabs/Tabs'
import {ImageSelectorEmbedTab} from './ImageSelectorEmbedTab'
import ImageSelectorSearchTabRoot from './ImageSelectorSearchTabRoot'
import {ImageSelectorUploadTab} from './ImageSelectorUploadTab'

interface Props {
Expand All @@ -13,47 +14,58 @@ const tabs = [
{
id: 'upload',
label: 'Upload',
Component: ImageSelectorUploadTab
Component: ImageSelectorUploadTab,
isVisible: true
},
{
id: 'embedLink',
label: 'Embed link',
Component: ImageSelectorEmbedTab
Component: ImageSelectorEmbedTab,
isVisible: true
},
{
id: 'addGif',
label: 'Add Gif',
Component: ImageSelectorSearchTabRoot,
isVisible: !!window.__ACTION__.GIF_PROVIDER
}
// {
// id: 'addGif',
// label: 'Add Gif',
// Component: ImageSelectorUploadTab
// }
] as const

export const ImageSelector = (props: Props) => {
const {editor} = props
const [activeIdx, setActiveIdx] = useState(0)
const {Component} = tabs[activeIdx]!
const setImageURL = (url: string) => {
const {from} = editor.state.selection
editor.chain().setImageBlock({src: url}).deleteRange({from, to: from}).focus().run()
const {to} = editor.state.selection
const size = editor.state.doc.content.size
let command = editor.chain().focus().setImageBlock({src: url})
if (size - to <= 1) {
// if we're at the end of the doc, add an extra paragraph to make it easier to click below
command = command.insertContent('<p></p>').setTextSelection(editor.state.selection.to + 1)
}
command.scrollIntoView().run()
}
return (
<div className='min-w-44 rounded-md bg-slate-100 p-2'>
<div className='flex h-full min-w-44 flex-col overflow-hidden rounded-md bg-slate-100 p-2'>
<Tabs activeIdx={activeIdx}>
{tabs.map((tab, idx) => (
<Tab
key={tab.label}
onClick={() => {
setActiveIdx(idx)
}}
className='whitespace-nowrap px-2 py-0'
label={
<div className='flex items-center justify-center text-sm font-normal'>
{tab.label}
</div>
}
/>
))}
{tabs
.filter((tab) => tab.isVisible)
.map((tab, idx) => (
<Tab
key={tab.label}
onClick={() => {
setActiveIdx(idx)
}}
className='whitespace-nowrap px-2 py-0'
label={
<div className='flex items-center justify-center text-sm font-normal'>
{tab.label}
</div>
}
/>
))}
</Tabs>
<Component setImageURL={setImageURL} />
<Component setImageURL={setImageURL} editor={editor} />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import type {Editor} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
import {useRef} from 'react'
import {usePaginationFragment, usePreloadedQuery, type PreloadedQuery} from 'react-relay'
import type {ImageSelectorSearchTabPaginationQuery} from '../../../__generated__/ImageSelectorSearchTabPaginationQuery.graphql'
import type {ImageSelectorSearchTabQuery} from '../../../__generated__/ImageSelectorSearchTabQuery.graphql'
import type {ImageSelectorSearchTabQuery_query$key} from '../../../__generated__/ImageSelectorSearchTabQuery_query.graphql'
import useLoadNextOnScrollBottom from '../../../hooks/useLoadNextOnScrollBottom'
import {cn} from '../../../ui/cn'

interface Props {
editor: Editor
queryRef: PreloadedQuery<ImageSelectorSearchTabQuery>
searchQuery: string
setSearchQuery: (query: string) => void
setImageURL: (url: string) => void
}

export const ImageSelectorSearchTab = (props: Props) => {
const {queryRef, setImageURL, searchQuery, setSearchQuery} = props
const ref = useRef<HTMLInputElement>(null)

const query = usePreloadedQuery<ImageSelectorSearchTabQuery>(
graphql`
query ImageSelectorSearchTabQuery($query: String!, $fetchOriginal: Boolean!) {
...ImageSelectorSearchTabQuery_query
}
`,
queryRef
)

const paginationRes = usePaginationFragment<
ImageSelectorSearchTabPaginationQuery,
ImageSelectorSearchTabQuery_query$key
>(
graphql`
fragment ImageSelectorSearchTabQuery_query on Query
@argumentDefinitions(after: {type: "String"}, first: {type: "Int", defaultValue: 20})
@refetchable(queryName: "ImageSelectorSearchTabPaginationQuery") {
searchGifs(query: $query, first: $first, after: $after)
@connection(key: "ImageSelectorSearchTabQuery_searchGifs") {
edges {
node {
previewUrl: url(size: tiny)
originalUrl: url(size: original) @include(if: $fetchOriginal)
}
}
}
}
`,
query
)
const {data} = paginationRes
const {searchGifs} = data
const {edges} = searchGifs!
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'
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const nextValue = e.target.value
setSearchQuery(nextValue)
}
const lastItem = useLoadNextOnScrollBottom(paginationRes, {}, 20)
return (
<div className='flex flex-col overflow-hidden'>
<form className='flex w-full min-w-44 flex-col items-center justify-center space-y-3 rounded-md bg-slate-100 p-2'>
<input
autoFocus
placeholder={placeholder}
value={searchQuery}
className='w-full outline-none focus:ring-2'
ref={ref}
onChange={onChange}
/>
</form>
<div className='grid w-96 auto-rows-[1px] grid-cols-[repeat(auto-fit,_minmax(112px,_1fr))] gap-x-1 overflow-auto'>
{edges.map((edge) => {
const {node} = edge
const {previewUrl, originalUrl} = node
return (
<button
key={previewUrl}
style={{gridRow: 'span 200'}} // initially too tall to prevent the lastItem from intersecting viewport
className={cn('row-span w-full cursor-pointer rounded')}
onClick={() => {
setImageURL(originalUrl || previewUrl)
}}
>
<img
src={previewUrl}
className='rounded'
onLoad={(e) => {
const img = e.target as HTMLImageElement
const button = img.parentElement!
button.style.setProperty('grid-row', `span ${img.height + 2}`)
}}
/>
</button>
)
})}
{lastItem}
</div>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {Editor} from '@tiptap/core'
import {Suspense, useState} from 'react'
import useQueryLoaderNow from '~/hooks/useQueryLoaderNow'
import type {ImageSelectorSearchTabQuery} from '../../../__generated__/ImageSelectorSearchTabQuery.graphql'
import imageSelectorSearchTabQuery from '../../../__generated__/ImageSelectorSearchTabQuery.graphql'
import {ImageSelectorSearchTab} from './ImageSelectorSearchTab'
interface Props {
editor: Editor
setImageURL: (url: string) => void
}

export const ImageSelectorSearchTabRoot = (props: Props) => {
const {editor} = props
const [searchQuery, setSearchQuery] = useState('')
const queryToSendToServer = searchQuery.length > 2 ? searchQuery : ''
const queryRef = useQueryLoaderNow<ImageSelectorSearchTabQuery>(
imageSelectorSearchTabQuery,
{
fetchOriginal: editor.storage.imageUpload.editorWidth > 500,
query: queryToSendToServer
},
undefined,
true
)

return (
<Suspense fallback={''}>
{queryRef && (
<ImageSelectorSearchTab
{...props}
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
queryRef={queryRef}
/>
)}
</Suspense>
)
}
export default ImageSelectorSearchTabRoot
25 changes: 13 additions & 12 deletions packages/client/tiptap/extensions/imageUpload/ImageUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import {EventEmitter} from 'eventemitter3'
import {ImageUploadBase} from '../../../shared/tiptap/extensions/ImageUploadBase'
import {ImageUploadView} from './ImageUploadView'

export const ImageUpload = ImageUploadBase.extend({
addStorage() {
export const ImageUpload = ImageUploadBase.extend<{editorWidth: number}>({
addOptions() {
return {
emitter: new EventEmitter()
editorWidth: 300
}
},
addStorage(this) {
return {
emitter: new EventEmitter(),
editorWidth: this.options.editorWidth
}
},

Expand All @@ -28,15 +34,10 @@ export const ImageUpload = ImageUploadBase.extend({
return {
setImageUpload:
() =>
({commands, editor}) => {
const to = editor.state.selection.to
const size = editor.state.doc.content.size
if (size - to <= 1) {
// if we're at the end of the doc, add an extra paragraph to make it easier to click below
return commands.insertContent(`<div data-type="${this.name}"></div><p></p>`)
} else {
return commands.insertContent(`<div data-type="${this.name}"></div>`)
}
({commands}) => {
// note: only call 1 command here. Calling multiple here & then having the caller also chaining commands
// will result in a fatal "Applying a mismatched transaction"
return commands.insertContent(`<div data-type="${this.name}"></div>`)
}
}
},
Expand Down
13 changes: 3 additions & 10 deletions packages/client/tiptap/extensions/imageUpload/ImageUploadView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const useHideWhenTriggerHidden = (setOpen: (open: boolean) => void) => {
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (!entry?.isIntersecting) {
if (entry && !entry?.isIntersecting && triggerRef.current) {
setOpen(false)
}
},
Expand Down Expand Up @@ -71,16 +71,9 @@ export const ImageUploadView = (props: NodeViewProps) => {
</div>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
asChild
align='start'
alignOffset={8}
onOpenAutoFocus={(e) => {
e.preventDefault()
}}
>
<Popover.Content asChild align='start' alignOffset={8} collisionPadding={8}>
{/* z-30 is for expanded reflection stacks using Zindex.DIALOG */}
<div className='absolute left-0 top-0 z-30'>
<div className='absolute left-0 top-0 z-30 flex max-h-[var(--radix-popper-available-height)] flex-col overflow-hidden'>
<ImageSelector editor={editor} />
</div>
</Popover.Content>
Expand Down
Loading

0 comments on commit d893258

Please sign in to comment.