From cb0ed7ba1328a988878d01c4214ffdfc705d9ba2 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 7 Oct 2025 16:07:21 +0200 Subject: [PATCH 01/11] basic setup --- .../components/fields/MarkdownInput/index.tsx | 16 ++++- .../src/components/popover/GifPopover.tsx | 63 +++++++++++++++++++ packages/shared/src/hooks/useGifQuery.ts | 20 ++++++ 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 packages/shared/src/components/popover/GifPopover.tsx create mode 100644 packages/shared/src/hooks/useGifQuery.ts diff --git a/packages/shared/src/components/fields/MarkdownInput/index.tsx b/packages/shared/src/components/fields/MarkdownInput/index.tsx index a93ee9a7d0..32b0085d8a 100644 --- a/packages/shared/src/components/fields/MarkdownInput/index.tsx +++ b/packages/shared/src/components/fields/MarkdownInput/index.tsx @@ -15,7 +15,13 @@ import React, { } from 'react'; import classNames from 'classnames'; import dynamic from 'next/dynamic'; -import { ImageIcon, MarkdownIcon, LinkIcon, AtIcon } from '../../icons'; +import { + ImageIcon, + MarkdownIcon, + LinkIcon, + AtIcon, + GiftIcon, +} from '../../icons'; import { Button, ButtonColor, @@ -44,6 +50,7 @@ import { usePopupSelector } from '../../../hooks/usePopupSelector'; import { focusInput } from '../../../lib/textarea'; import CloseButton from '../../CloseButton'; import { ACCEPTED_TYPES } from '../../../graphql/posts'; +import GifPopover from '../../popover/GifPopover'; const RecommendedEmojiTooltip = dynamic( () => @@ -389,6 +396,13 @@ function MarkdownInput( /> )} + , + }} + /> {shouldShowSubmit && ( ))} )} +
); diff --git a/packages/shared/src/hooks/input/useMarkdownInput.ts b/packages/shared/src/hooks/input/useMarkdownInput.ts index cdd1953d98..0a915d55be 100644 --- a/packages/shared/src/hooks/input/useMarkdownInput.ts +++ b/packages/shared/src/hooks/input/useMarkdownInput.ts @@ -53,6 +53,7 @@ export enum MarkdownCommand { Link = 'link', Mention = 'mention', Emoji = 'emoji', + Gif = 'gif', } export interface UseMarkdownInputProps @@ -81,6 +82,7 @@ export interface UseMarkdownInput { onLinkCommand?: () => Promise; onMentionCommand?: () => Promise; onUploadCommand?: (files: FileList) => void; + onGifCommand?: (gifUrl: string, altText: string) => Promise; onApplyMention?: (user: UserShortProfile) => Promise; onApplyEmoji?: (emoji: string) => Promise; checkMention?: (position?: number[]) => void; @@ -98,6 +100,7 @@ export const defaultMarkdownCommands = { link: true, mention: true, emoji: true, + gif: true, }; const specialCharsRegex = new RegExp(/[^A-Za-z0-9_.]/); @@ -117,6 +120,7 @@ export const useMarkdownInput = ({ const isUploadEnabled = enabledCommand[MarkdownCommand.Upload]; const isMentionEnabled = enabledCommand[MarkdownCommand.Mention]; const isEmojiEnabled = enabledCommand[MarkdownCommand.Emoji]; + const isGifEnabled = enabledCommand[MarkdownCommand.Gif]; const [command, setCommand] = useState(); const [input, setInput] = useState(initialContent); const [query, setQuery] = useState(undefined); @@ -432,6 +436,16 @@ export const useMarkdownInput = ({ startUploading(); }; + const onGifCommand = async (gifUrl: string, altText: string) => { + const replace: GetReplacementFn = (type, { trailingChar }) => { + const replacement = `${ + !trailingChar ? '' : '\n\n' + }![${altText}](${gifUrl})\n\n`; + return { replacement }; + }; + await command.replaceWord(replace, onUpdate); + }; + const onPaste: ClipboardEventHandler = async (e) => { const pastedText = e.clipboardData.getData('text'); if (isValidHttpUrl(pastedText)) { @@ -492,11 +506,13 @@ export const useMarkdownInput = ({ onCloseEmoji, } : {}; + const gifProps = isGifEnabled ? { onGifCommand } : {}; return { ...uploadProps, ...mentionProps, ...emojiProps, + ...gifProps, input, onLinkCommand: isLinkEnabled ? onLinkCommand : null, callbacks: { diff --git a/packages/shared/src/hooks/useGifQuery.ts b/packages/shared/src/hooks/useGifQuery.ts index e0d0f3caf0..407100defd 100644 --- a/packages/shared/src/hooks/useGifQuery.ts +++ b/packages/shared/src/hooks/useGifQuery.ts @@ -1,19 +1,54 @@ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; import { apiUrl } from '../lib/config'; +import { RequestKey } from '../lib/query'; -const useGifQuery = ({ query, limit }) => { - const { data, isLoading } = useQuery({ - queryKey: ['gifQuery', query, limit], - queryFn: async () => { - const response = await fetch(`${apiUrl}/gifs?q=${query}&limit=${limit}`); - return response.json(); - }, - enabled: !!query, - }); +export type GifData = { + id: string; + title: string; + preview: string; + url: string; +}; + +interface GifResponse { + gifs: GifData[]; + next?: string; +} + +const useGifQuery = ({ + query, + limit = '10', +}: { + query: string; + limit?: string; +}) => { + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: [RequestKey.Gif, query, limit], + queryFn: async ({ pageParam }) => { + const params = new URLSearchParams({ + q: query, + limit: limit.toString(), + ...(pageParam && { pos: pageParam as string }), + }); + + const response = await fetch(`${apiUrl}/gifs?${params}`); + return response.json(); + }, + getNextPageParam: (lastPage) => { + return lastPage.next || undefined; + }, + initialPageParam: undefined, + enabled: !!query, + }); + + const gifs = data?.pages.flatMap((page) => page.gifs) ?? []; return { - data, + data: gifs, isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, }; }; diff --git a/packages/shared/src/lib/query.ts b/packages/shared/src/lib/query.ts index 942a07d53e..360170bc1d 100644 --- a/packages/shared/src/lib/query.ts +++ b/packages/shared/src/lib/query.ts @@ -217,6 +217,7 @@ export enum RequestKey { Opportunity = 'opportunity', UserCandidatePreferences = 'user_candidate_preferences', KeywordAutocomplete = 'keyword_autocomplete', + Gif = 'gif', } export const getPostByIdKey = (id: string): QueryKey => [RequestKey.Post, id]; From 6ae87c9012d3cd21dc676fd9fd28f28c16df420a Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Wed, 8 Oct 2025 09:16:39 +0200 Subject: [PATCH 03/11] introduce favs --- .../src/components/popover/GifPopover.tsx | 51 +++++++---- packages/shared/src/hooks/useGif.ts | 85 +++++++++++++++++++ packages/shared/src/hooks/useGifQuery.ts | 55 ------------ 3 files changed, 120 insertions(+), 71 deletions(-) create mode 100644 packages/shared/src/hooks/useGif.ts delete mode 100644 packages/shared/src/hooks/useGifQuery.ts diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index 7f26528157..28aab466b2 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -7,7 +7,8 @@ import { PopoverContent } from './Popover'; import { Typography, TypographyType } from '../typography/Typography'; import { TextField } from '../fields/TextField'; import useDebounceFn from '../../hooks/useDebounceFn'; -import useGifQuery from '../../hooks/useGifQuery'; +import useGif from '../../hooks/useGif'; +import { StarIcon } from '../icons'; const searchSuggestions = [ 'Nodding zoom', @@ -47,7 +48,14 @@ const GifPopover = ({ (value) => setQuery(value), 300, ); - const { data, isLoading, fetchNextPage, isFetchingNextPage } = useGifQuery({ + const { + data, + isLoading, + fetchNextPage, + isFetchingNextPage, + favorite, + favorites, + } = useGif({ query, limit: '20', }); @@ -111,20 +119,31 @@ const GifPopover = ({ {data?.length > 0 && !isLoading && (
{data.map((gif) => ( - +
+
+
+ +
))}
)} diff --git a/packages/shared/src/hooks/useGif.ts b/packages/shared/src/hooks/useGif.ts new file mode 100644 index 0000000000..d69f2d1abd --- /dev/null +++ b/packages/shared/src/hooks/useGif.ts @@ -0,0 +1,85 @@ +import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { apiUrl } from '../lib/config'; +import { generateQueryKey, RequestKey } from '../lib/query'; +import { useAuthContext } from '../contexts/AuthContext'; + +export type GifData = { + id: string; + title: string; + preview: string; + url: string; +}; + +interface GifResponse { + gifs: GifData[]; + next?: string; +} + +const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { + const { user } = useAuthContext(); + const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: generateQueryKey(RequestKey.Gif, user, { + query, + limit, + }), + queryFn: async ({ pageParam }) => { + const params = new URLSearchParams({ + q: query, + limit: limit.toString(), + ...(pageParam && { pos: pageParam as string }), + }); + + const response = await fetch(`${apiUrl}/gifs?${params}`); + return response.json(); + }, + getNextPageParam: (lastPage) => { + return lastPage.next || undefined; + }, + initialPageParam: undefined, + enabled: !!query, + }); + const gifs = data?.pages.flatMap((page) => page.gifs) ?? []; + const { data: favData } = useQuery({ + queryKey: generateQueryKey(RequestKey.Gif, user, 'favorites'), + queryFn: async () => { + const response = await fetch(`${apiUrl}/gifs/favorites`, { + credentials: 'include', + }); + if (!response.ok) { + throw new Error('Failed to fetch favorite gifs'); + } + return response.json(); + }, + }); + + const { mutate } = useMutation({ + mutationKey: generateQueryKey(RequestKey.Gif, user, 'favorites'), + mutationFn: async (gif: GifData) => { + const response = await fetch(`${apiUrl}/gifs/favorite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(gif), + }); + if (!response.ok) { + throw new Error('Failed to favorite gif'); + } + return response.json(); + }, + }); + + return { + data: gifs, + isLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + favorite: mutate, + favorites: favData?.favorites || [], + }; +}; + +export default useGif; diff --git a/packages/shared/src/hooks/useGifQuery.ts b/packages/shared/src/hooks/useGifQuery.ts deleted file mode 100644 index 407100defd..0000000000 --- a/packages/shared/src/hooks/useGifQuery.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; -import { apiUrl } from '../lib/config'; -import { RequestKey } from '../lib/query'; - -export type GifData = { - id: string; - title: string; - preview: string; - url: string; -}; - -interface GifResponse { - gifs: GifData[]; - next?: string; -} - -const useGifQuery = ({ - query, - limit = '10', -}: { - query: string; - limit?: string; -}) => { - const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = - useInfiniteQuery({ - queryKey: [RequestKey.Gif, query, limit], - queryFn: async ({ pageParam }) => { - const params = new URLSearchParams({ - q: query, - limit: limit.toString(), - ...(pageParam && { pos: pageParam as string }), - }); - - const response = await fetch(`${apiUrl}/gifs?${params}`); - return response.json(); - }, - getNextPageParam: (lastPage) => { - return lastPage.next || undefined; - }, - initialPageParam: undefined, - enabled: !!query, - }); - - const gifs = data?.pages.flatMap((page) => page.gifs) ?? []; - - return { - data: gifs, - isLoading, - fetchNextPage, - hasNextPage, - isFetchingNextPage, - }; -}; - -export default useGifQuery; From 4cdc38ea1ab80280af85de5e37703847580761d3 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Sun, 14 Dec 2025 09:16:39 +0800 Subject: [PATCH 04/11] add favorites --- .../src/components/popover/GifPopover.tsx | 27 ++++--- packages/shared/src/hooks/useGif.ts | 72 +++++++++++++++---- 2 files changed, 73 insertions(+), 26 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index 28aab466b2..de2ddb83f5 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -4,7 +4,6 @@ import { useInView } from 'react-intersection-observer'; import type { ButtonProps } from '../buttons/Button'; import { Button } from '../buttons/Button'; import { PopoverContent } from './Popover'; -import { Typography, TypographyType } from '../typography/Typography'; import { TextField } from '../fields/TextField'; import useDebounceFn from '../../hooks/useDebounceFn'; import useGif from '../../hooks/useGif'; @@ -46,7 +45,7 @@ const GifPopover = ({ ]); const [debounceQuery] = useDebounceFn( (value) => setQuery(value), - 300, + 500, ); const { data, @@ -55,6 +54,7 @@ const GifPopover = ({ isFetchingNextPage, favorite, favorites, + isFetchingFavorites, } = useGif({ query, limit: '20', @@ -64,9 +64,14 @@ const GifPopover = ({ if (inView && data?.length > 0 && !isFetchingNextPage && query) { fetchNextPage(); } + // No need to update on query change // eslint-disable-next-line react-hooks/exhaustive-deps }, [inView, isLoading, isFetchingNextPage, fetchNextPage]); + const showingFavorites = !query; + const gifsToDisplay = showingFavorites ? favorites : data; + const isLoadingGifs = showingFavorites ? isFetchingFavorites : isLoading; + const handleOpenChange = (isOpen: boolean) => { if (isOpen && textareaRef?.current) { setSavedSelection([ @@ -79,9 +84,12 @@ const GifPopover = ({ const handleGifClick = async (gif: { url: string; title: string }) => { if (textareaRef?.current) { - textareaRef.current.focus(); - textareaRef.current.selectionStart = savedSelection[0]; - textareaRef.current.selectionEnd = savedSelection[1]; + const [selectionStart, selectionEnd] = savedSelection; + const currentTextarea = textareaRef.current; + + currentTextarea.focus(); + currentTextarea.selectionStart = selectionStart; + currentTextarea.selectionEnd = selectionEnd; } await onGifCommand?.(gif.url, gif.title); @@ -113,12 +121,9 @@ const GifPopover = ({ } />
- {!query && ( - Search Tenor - )} - {data?.length > 0 && !isLoading && ( + {gifsToDisplay?.length > 0 && !isLoadingGifs && (
- {data.map((gif) => ( + {gifsToDisplay.map((gif) => (
diff --git a/packages/shared/src/hooks/useGif.ts b/packages/shared/src/hooks/useGif.ts index d69f2d1abd..9411de28be 100644 --- a/packages/shared/src/hooks/useGif.ts +++ b/packages/shared/src/hooks/useGif.ts @@ -1,4 +1,9 @@ -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { + useInfiniteQuery, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/react-query'; import { apiUrl } from '../lib/config'; import { generateQueryKey, RequestKey } from '../lib/query'; import { useAuthContext } from '../contexts/AuthContext'; @@ -17,6 +22,8 @@ interface GifResponse { const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { const { user } = useAuthContext(); + const queryClient = useQueryClient(); + const favoritesQueryKey = generateQueryKey(RequestKey.Gif, user, 'favorites'); const { data, isLoading, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: generateQueryKey(RequestKey.Gif, user, { @@ -40,20 +47,28 @@ const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { enabled: !!query, }); const gifs = data?.pages.flatMap((page) => page.gifs) ?? []; - const { data: favData } = useQuery({ - queryKey: generateQueryKey(RequestKey.Gif, user, 'favorites'), - queryFn: async () => { - const response = await fetch(`${apiUrl}/gifs/favorites`, { - credentials: 'include', - }); - if (!response.ok) { - throw new Error('Failed to fetch favorite gifs'); - } - return response.json(); - }, - }); + const { data: favData, isFetching: isFetchingFavorites } = + useQuery({ + queryKey: favoritesQueryKey, + queryFn: async () => { + const response = await fetch(`${apiUrl}/gifs/favorites`, { + credentials: 'include', + }); + if (!response.ok) { + throw new Error('Failed to fetch favorite gifs'); + } + return await response.json(); + }, + enabled: true, + }); + const favorites = favData?.gifs ?? []; - const { mutate } = useMutation({ + const { mutate } = useMutation< + unknown, + Error, + GifData, + { previousFavorites?: GifResponse } + >({ mutationKey: generateQueryKey(RequestKey.Gif, user, 'favorites'), mutationFn: async (gif: GifData) => { const response = await fetch(`${apiUrl}/gifs/favorite`, { @@ -69,6 +84,32 @@ const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { } return response.json(); }, + onMutate: async (gif) => { + await queryClient.cancelQueries({ queryKey: favoritesQueryKey }); + + const previousFavorites = + queryClient.getQueryData(favoritesQueryKey); + const prevGifs = previousFavorites?.gifs ?? []; + const isFavorite = prevGifs.some(({ id }) => id === gif.id); + const updatedGifs = isFavorite + ? prevGifs.filter(({ id }) => id !== gif.id) + : [gif, ...prevGifs]; + + queryClient.setQueryData( + favoritesQueryKey, + (existingFavorites) => ({ + gifs: updatedGifs, + next: existingFavorites?.next, + }), + ); + + return { previousFavorites }; + }, + onError: (_error, _gif, context) => { + if (context?.previousFavorites) { + queryClient.setQueryData(favoritesQueryKey, context.previousFavorites); + } + }, }); return { @@ -78,7 +119,8 @@ const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { hasNextPage, isFetchingNextPage, favorite: mutate, - favorites: favData?.favorites || [], + favorites, + isFetchingFavorites, }; }; From 86c5eb64a820053083aa8b73b8033d09156e809b Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Sun, 14 Dec 2025 14:30:16 +0800 Subject: [PATCH 05/11] update responsiveness --- .../src/components/popover/GifPopover.tsx | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index de2ddb83f5..8f2d5953f2 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -8,6 +8,11 @@ import { TextField } from '../fields/TextField'; import useDebounceFn from '../../hooks/useDebounceFn'; import useGif from '../../hooks/useGif'; import { StarIcon } from '../icons'; +import { + Typography, + TypographyColor, + TypographyType, +} from '../typography/Typography'; const searchSuggestions = [ 'Nodding zoom', @@ -106,9 +111,9 @@ const GifPopover = ({ side="top" align="start" avoidCollisions - className="h-[25rem] w-[31.25rem] overflow-y-scroll rounded-16 border border-border-subtlest-tertiary bg-background-popover p-4 data-[side=bottom]:mt-1 data-[side=top]:mb-1" + className="flex h-[25rem] w-screen flex-col rounded-16 border border-border-subtlest-tertiary bg-background-popover p-4 data-[side=bottom]:mt-1 data-[side=top]:mb-1 tablet:w-[31.25rem]" > -
+
debounceQuery(e.target.value)} @@ -121,38 +126,54 @@ const GifPopover = ({ } />
- {gifsToDisplay?.length > 0 && !isLoadingGifs && ( -
- {gifsToDisplay.map((gif) => ( -
-
-
+
- -
- ))} -
- )} -
+ ))} +
+ )} + {!isLoadingGifs && (!gifsToDisplay || gifsToDisplay.length === 0) && ( +
+ + {showingFavorites + ? 'You have no favorites yet. Add some, and they will appear here!' + : 'no results matching your search 😞'} + +
+ )} +
+
); From 2718b825ad72fa40e5232bbe17c1843984d1e714 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 16 Dec 2025 12:38:21 +0700 Subject: [PATCH 06/11] add gif icon --- .../components/fields/MarkdownInput/index.tsx | 11 +++------ .../src/components/icons/Gif/filled.svg | 5 ++++ .../shared/src/components/icons/Gif/index.tsx | 10 ++++++++ .../src/components/icons/Gif/outlined.svg | 10 ++++++++ .../src/components/popover/GifPopover.tsx | 24 ++++++++++--------- 5 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 packages/shared/src/components/icons/Gif/filled.svg create mode 100644 packages/shared/src/components/icons/Gif/index.tsx create mode 100644 packages/shared/src/components/icons/Gif/outlined.svg diff --git a/packages/shared/src/components/fields/MarkdownInput/index.tsx b/packages/shared/src/components/fields/MarkdownInput/index.tsx index f242485bc6..8b4ceebaec 100644 --- a/packages/shared/src/components/fields/MarkdownInput/index.tsx +++ b/packages/shared/src/components/fields/MarkdownInput/index.tsx @@ -15,13 +15,7 @@ import React, { } from 'react'; import classNames from 'classnames'; import dynamic from 'next/dynamic'; -import { - ImageIcon, - MarkdownIcon, - LinkIcon, - AtIcon, - GiftIcon, -} from '../../icons'; +import { ImageIcon, MarkdownIcon, LinkIcon, AtIcon } from '../../icons'; import { Button, ButtonColor, @@ -51,6 +45,7 @@ import { focusInput } from '../../../lib/textarea'; import CloseButton from '../../CloseButton'; import { ACCEPTED_TYPES } from '../../../graphql/posts'; import GifPopover from '../../popover/GifPopover'; +import { GifIcon } from '../../icons/Gif'; const RecommendedEmojiTooltip = dynamic( () => @@ -401,7 +396,7 @@ function MarkdownInput( buttonProps={{ size: actionButtonSizes, variant: ButtonVariant.Tertiary, - icon: , + icon: , }} onGifCommand={onGifCommand} textareaRef={textareaRef} diff --git a/packages/shared/src/components/icons/Gif/filled.svg b/packages/shared/src/components/icons/Gif/filled.svg new file mode 100644 index 0000000000..9ee742be7f --- /dev/null +++ b/packages/shared/src/components/icons/Gif/filled.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/shared/src/components/icons/Gif/index.tsx b/packages/shared/src/components/icons/Gif/index.tsx new file mode 100644 index 0000000000..2f115f4367 --- /dev/null +++ b/packages/shared/src/components/icons/Gif/index.tsx @@ -0,0 +1,10 @@ +import type { ReactElement } from 'react'; +import React from 'react'; +import type { IconProps } from '../../Icon'; +import Icon from '../../Icon'; +import OutlinedIcon from './outlined.svg'; +import FilledIcon from './filled.svg'; + +export const GifIcon = (props: IconProps): ReactElement => ( + +); diff --git a/packages/shared/src/components/icons/Gif/outlined.svg b/packages/shared/src/components/icons/Gif/outlined.svg new file mode 100644 index 0000000000..b4a4e160fb --- /dev/null +++ b/packages/shared/src/components/icons/Gif/outlined.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index 8f2d5953f2..bdaf22e7c2 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -1,5 +1,5 @@ import { Popover, PopoverTrigger } from '@radix-ui/react-popover'; -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useInView } from 'react-intersection-observer'; import type { ButtonProps } from '../buttons/Button'; import { Button } from '../buttons/Button'; @@ -41,7 +41,7 @@ const GifPopover = ({ }: GifPopoverProps) => { const { ref: scrollRef, inView } = useInView({ rootMargin: '20px', - threshold: 1, + threshold: 0.5, }); const [open, setOpen] = useState(false); const [query, setQuery] = useState(''); @@ -64,14 +64,11 @@ const GifPopover = ({ query, limit: '20', }); - - useEffect(() => { + const [debounceNextPage] = useDebounceFn(() => { if (inView && data?.length > 0 && !isFetchingNextPage && query) { fetchNextPage(); } - // No need to update on query change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [inView, isLoading, isFetchingNextPage, fetchNextPage]); + }, 500); const showingFavorites = !query; const gifsToDisplay = showingFavorites ? favorites : data; @@ -126,11 +123,17 @@ const GifPopover = ({ } />
-
+
debounceNextPage()} + > {!isLoadingGifs && gifsToDisplay?.length > 0 && (
- {gifsToDisplay.map((gif) => ( -
+ {gifsToDisplay.map((gif, idx) => ( +
)} -
From 8aba33a0e855a3bee14cfacadd48621374247784 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 16 Dec 2025 12:43:08 +0700 Subject: [PATCH 07/11] only load favorites on popover open --- .../shared/src/components/popover/GifPopover.tsx | 2 ++ packages/shared/src/hooks/useGif.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index bdaf22e7c2..e00970cce9 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -63,6 +63,7 @@ const GifPopover = ({ } = useGif({ query, limit: '20', + favoritesEnabled: open, }); const [debounceNextPage] = useDebounceFn(() => { if (inView && data?.length > 0 && !isFetchingNextPage && query) { @@ -131,6 +132,7 @@ const GifPopover = ({
{gifsToDisplay.map((gif, idx) => (
diff --git a/packages/shared/src/hooks/useGif.ts b/packages/shared/src/hooks/useGif.ts index 9411de28be..b0ce771445 100644 --- a/packages/shared/src/hooks/useGif.ts +++ b/packages/shared/src/hooks/useGif.ts @@ -20,7 +20,15 @@ interface GifResponse { next?: string; } -const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { +const useGif = ({ + query, + limit = '10', + favoritesEnabled = true, +}: { + query: string; + limit?: string; + favoritesEnabled?: boolean; +}) => { const { user } = useAuthContext(); const queryClient = useQueryClient(); const favoritesQueryKey = generateQueryKey(RequestKey.Gif, user, 'favorites'); @@ -59,7 +67,7 @@ const useGif = ({ query, limit = '10' }: { query: string; limit?: string }) => { } return await response.json(); }, - enabled: true, + enabled: favoritesEnabled, }); const favorites = favData?.gifs ?? []; From 7a6d88e634fe864ca2c77b2f6b22342a7ebaae6e Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Tue, 16 Dec 2025 16:20:19 +0700 Subject: [PATCH 08/11] typo --- packages/shared/src/components/popover/GifPopover.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index e00970cce9..e6183ed4cc 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -22,10 +22,9 @@ const searchSuggestions = [ 'Mr Bean waiting', 'Dancing dog', 'Eye roll', - 'Runescape gnome', - 'Tzuyu from TWICE', + 'TWICE', 'Hackerman', - 'Steve Carel cheers', + 'Steve Carell cheers', ]; type GifPopoverProps = { From 20c5cb6eed9d588ce584504e1652965eccdbe7da Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Thu, 18 Dec 2025 01:08:10 +0700 Subject: [PATCH 09/11] include credential --- packages/shared/src/hooks/useGif.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/hooks/useGif.ts b/packages/shared/src/hooks/useGif.ts index b0ce771445..dfd231dbb2 100644 --- a/packages/shared/src/hooks/useGif.ts +++ b/packages/shared/src/hooks/useGif.ts @@ -45,7 +45,9 @@ const useGif = ({ ...(pageParam && { pos: pageParam as string }), }); - const response = await fetch(`${apiUrl}/gifs?${params}`); + const response = await fetch(`${apiUrl}/gifs?${params}`, { + credentials: 'include', + }); return response.json(); }, getNextPageParam: (lastPage) => { From 1b0fcf226a28a03429a2a5e66c06d6dd960e401d Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Thu, 18 Dec 2025 12:32:01 +0700 Subject: [PATCH 10/11] add spinner to bottom to indicate loading --- .../src/components/popover/GifPopover.tsx | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index e6183ed4cc..f99a3123ba 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -13,6 +13,8 @@ import { TypographyColor, TypographyType, } from '../typography/Typography'; +import { GenericLoaderSpinner } from '../utilities/loaders'; +import { IconSize } from '../Icon'; const searchSuggestions = [ 'Nodding zoom', @@ -39,7 +41,6 @@ const GifPopover = ({ textareaRef, }: GifPopoverProps) => { const { ref: scrollRef, inView } = useInView({ - rootMargin: '20px', threshold: 0.5, }); const [open, setOpen] = useState(false); @@ -128,40 +129,45 @@ const GifPopover = ({ onScroll={() => debounceNextPage()} > {!isLoadingGifs && gifsToDisplay?.length > 0 && ( -
- {gifsToDisplay.map((gif, idx) => ( -
-
-
+
- -
- ))} -
+ ))} +
+
+ +
+ )} {!isLoadingGifs && (!gifsToDisplay || gifsToDisplay.length === 0) && (
From 65e30a74d0e15ead9a7d19b8265799dabf31ed58 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Thu, 18 Dec 2025 12:57:28 +0700 Subject: [PATCH 11/11] set staleTime to infinite on favorites --- .../shared/src/components/popover/GifPopover.tsx | 15 ++++++++------- packages/shared/src/hooks/useGif.ts | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/components/popover/GifPopover.tsx b/packages/shared/src/components/popover/GifPopover.tsx index f99a3123ba..9a33a0dc83 100644 --- a/packages/shared/src/components/popover/GifPopover.tsx +++ b/packages/shared/src/components/popover/GifPopover.tsx @@ -71,9 +71,8 @@ const GifPopover = ({ } }, 500); - const showingFavorites = !query; - const gifsToDisplay = showingFavorites ? favorites : data; - const isLoadingGifs = showingFavorites ? isFetchingFavorites : isLoading; + const gifsToDisplay = !query ? favorites : data; + const isLoadingGifs = !query ? isFetchingFavorites : isLoading; const handleOpenChange = (isOpen: boolean) => { if (isOpen && textareaRef?.current) { @@ -164,9 +163,11 @@ const GifPopover = ({
))}
-
- -
+ {query && ( +
+ +
+ )} )} {!isLoadingGifs && (!gifsToDisplay || gifsToDisplay.length === 0) && ( @@ -176,7 +177,7 @@ const GifPopover = ({ color={TypographyColor.Tertiary} className="w-full min-w-0 whitespace-normal break-words text-center" > - {showingFavorites + {!query ? 'You have no favorites yet. Add some, and they will appear here!' : 'no results matching your search 😞'} diff --git a/packages/shared/src/hooks/useGif.ts b/packages/shared/src/hooks/useGif.ts index dfd231dbb2..57543ad1c4 100644 --- a/packages/shared/src/hooks/useGif.ts +++ b/packages/shared/src/hooks/useGif.ts @@ -70,6 +70,7 @@ const useGif = ({ return await response.json(); }, enabled: favoritesEnabled, + staleTime: Infinity, }); const favorites = favData?.gifs ?? [];