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: gifabol (tenor search) #10735

Merged
merged 6 commits into from
Jan 28, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,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
Loading