From a5501977af5c8e1bc35bf65f1810a8d56e351287 Mon Sep 17 00:00:00 2001 From: Ido Shamun <1993245+idoshamun@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:17:58 +0200 Subject: [PATCH 1/6] fix(org): reduce organization settings query payload (#5538) --- .../components/InviteMemberModal.tsx | 4 +- .../src/features/organizations/graphql.ts | 77 ++++++++++++++----- .../organizations/hooks/useOrganization.ts | 18 ++++- .../hooks/useOrganizationSubscription.ts | 9 ++- .../organizations/hooks/useOrganizations.ts | 4 +- .../settings/organization/[orgId]/members.tsx | 8 +- 6 files changed, 93 insertions(+), 27 deletions(-) diff --git a/packages/shared/src/features/organizations/components/InviteMemberModal.tsx b/packages/shared/src/features/organizations/components/InviteMemberModal.tsx index 851dfe85c9..573ccdfde2 100644 --- a/packages/shared/src/features/organizations/components/InviteMemberModal.tsx +++ b/packages/shared/src/features/organizations/components/InviteMemberModal.tsx @@ -33,7 +33,9 @@ export const InviteMemberModal = ({ const [isCopying, copyLink] = useCopyLink(); const { openModal } = useLazyModal(); - const { organization, referralUrl, seats } = useOrganization(organizationId); + const { organization, referralUrl, seats } = useOrganization(organizationId, { + includeMembers: true, + }); const isMobile = useViewSize(ViewSize.MobileL); diff --git a/packages/shared/src/features/organizations/graphql.ts b/packages/shared/src/features/organizations/graphql.ts index 05bb1a3abe..8567a5ba54 100644 --- a/packages/shared/src/features/organizations/graphql.ts +++ b/packages/shared/src/features/organizations/graphql.ts @@ -25,31 +25,72 @@ export const ORGANIZATION_SHORT_FRAGMENT = gql` } `; -export const ORGANIZATION_FRAGMENT = gql` - fragment OrganizationFragment on Organization { +export const ORGANIZATION_BASE_FRAGMENT = gql` + fragment OrganizationBaseFragment on Organization { ...OrganizationShortFragment seats activeSeats status + } + + ${ORGANIZATION_SHORT_FRAGMENT} +`; +export const ORGANIZATION_FRAGMENT = gql` + fragment OrganizationFragment on Organization { + ...OrganizationBaseFragment members { ...OrganizationMemberFragment } } - ${ORGANIZATION_SHORT_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} ${ORGANIZATION_MEMBER_FRAGMENT} `; -export const USER_ORGANIZATION_FRAGMENT = gql` - fragment UserOrganizationFragment on UserOrganization { +export const USER_ORGANIZATION_BASE_FRAGMENT = gql` + fragment UserOrganizationBaseFragment on UserOrganization { role referralToken - referralUrl seatType } `; +export const USER_ORGANIZATION_FRAGMENT = gql` + fragment UserOrganizationFragment on UserOrganization { + ...UserOrganizationBaseFragment + referralUrl + } + + ${USER_ORGANIZATION_BASE_FRAGMENT} +`; + +export const ORGANIZATIONS_BASE_QUERY = gql` + query OrganizationsBase { + organizations { + ...UserOrganizationBaseFragment + organization { + ...OrganizationShortFragment + } + } + } + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_SHORT_FRAGMENT} +`; + +export const ORGANIZATION_BASE_QUERY = gql` + query OrganizationBase($id: ID!) { + organization(id: $id) { + ...UserOrganizationBaseFragment + organization { + ...OrganizationBaseFragment + } + } + } + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} +`; + export const ORGANIZATIONS_QUERY = gql` query Organizations { organizations { @@ -98,27 +139,27 @@ export const GET_ORGANIZATION_BY_ID_AND_INVITE_TOKEN_QUERY = gql` export const UPDATE_ORGANIZATION_MUTATION = gql` mutation UpdateOrganization($id: ID!, $name: String, $image: Upload) { updateOrganization(id: $id, name: $name, image: $image) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const JOIN_ORGANIZATION_MUTATION = gql` mutation JoinOrganization($id: ID!, $token: String!) { joinOrganization(id: $id, token: $token) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const LEAVE_ORGANIZATION_MUTATION = gql` @@ -132,14 +173,14 @@ export const LEAVE_ORGANIZATION_MUTATION = gql` export const UPDATE_ORGANIZATION_SUBSCRIPTION_MUTATION = gql` mutation UpdateOrganizationSubscription($id: ID!, $quantity: Int!) { updateOrganizationSubscription(id: $id, quantity: $quantity) { - ...UserOrganizationFragment + ...UserOrganizationBaseFragment organization { - ...OrganizationFragment + ...OrganizationBaseFragment } } } - ${USER_ORGANIZATION_FRAGMENT} - ${ORGANIZATION_FRAGMENT} + ${USER_ORGANIZATION_BASE_FRAGMENT} + ${ORGANIZATION_BASE_FRAGMENT} `; export const PREVIEW_SUBSCRIPTION_UPDATE_QUERY = gql` diff --git a/packages/shared/src/features/organizations/hooks/useOrganization.ts b/packages/shared/src/features/organizations/hooks/useOrganization.ts index 2afff04600..9240a82186 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganization.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganization.ts @@ -6,6 +6,7 @@ import { DEFAULT_ERROR, gqlClient } from '../../../graphql/common'; import { DELETE_ORGANIZATION_MUTATION, JOIN_ORGANIZATION_MUTATION, + ORGANIZATION_BASE_QUERY, LEAVE_ORGANIZATION_MUTATION, ORGANIZATION_QUERY, REMOVE_ORGANIZATION_MEMBER_MUTATION, @@ -125,13 +126,22 @@ export const joinOrganizationHandler = async ({ export const useOrganization = ( organizationId: string, - queryOptions?: Partial>, + options?: Partial> & { + includeMembers?: boolean; + }, ) => { const router = useRouter(); const { displayToast } = useToastNotification(); const { user, isAuthReady, refetchBoot } = useAuthContext(); + const { includeMembers = false, ...queryOptions } = options || {}; + const queryMode = includeMembers ? 'members' : 'base'; + const query = includeMembers ? ORGANIZATION_QUERY : ORGANIZATION_BASE_QUERY; const enableQuery = !!organizationId && !!user && isAuthReady; - const queryKey = generateOrganizationQueryKey(user, organizationId); + const queryKey = generateOrganizationQueryKey( + user, + organizationId, + queryMode, + ); const queryClient = useQueryClient(); const { data, isFetching } = useQuery({ @@ -139,7 +149,9 @@ export const useOrganization = ( queryFn: async () => { const res = await gqlClient.request<{ organization: UserOrganization; - }>(ORGANIZATION_QUERY, { id: organizationId }); + }>(query, { + id: organizationId, + }); return res?.organization || null; }, diff --git a/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts b/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts index 5e1308c065..dd2383707f 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganizationSubscription.ts @@ -97,9 +97,16 @@ export const useOrganizationSubscription = ( }, onSuccess: async (res) => { await queryClient.setQueryData( - generateOrganizationQueryKey(user, organizationId), + generateOrganizationQueryKey(user, organizationId, 'base'), () => res, ); + await queryClient.invalidateQueries({ + queryKey: generateOrganizationQueryKey( + user, + organizationId, + 'members', + ), + }); router.push(getOrganizationSettingsUrl(organizationId, 'members')); displayToast('The organization has been updated'); diff --git a/packages/shared/src/features/organizations/hooks/useOrganizations.ts b/packages/shared/src/features/organizations/hooks/useOrganizations.ts index 65b9f8e2d2..97cc184e26 100644 --- a/packages/shared/src/features/organizations/hooks/useOrganizations.ts +++ b/packages/shared/src/features/organizations/hooks/useOrganizations.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { gqlClient } from '../../../graphql/common'; -import { ORGANIZATIONS_QUERY } from '../graphql'; +import { ORGANIZATIONS_BASE_QUERY } from '../graphql'; import type { UserOrganization } from '../types'; import { generateQueryKey, RequestKey, StaleTime } from '../../../lib/query'; import { useAuthContext } from '../../../contexts/AuthContext'; @@ -13,7 +13,7 @@ export const useOrganizations = () => { queryFn: async () => { const data = await gqlClient.request<{ organizations: UserOrganization[]; - }>(ORGANIZATIONS_QUERY); + }>(ORGANIZATIONS_BASE_QUERY); if (!data || !data.organizations) { return []; diff --git a/packages/webapp/pages/settings/organization/[orgId]/members.tsx b/packages/webapp/pages/settings/organization/[orgId]/members.tsx index ac353886b6..39d24f04f3 100644 --- a/packages/webapp/pages/settings/organization/[orgId]/members.tsx +++ b/packages/webapp/pages/settings/organization/[orgId]/members.tsx @@ -80,7 +80,9 @@ const OrganizationOptionsMenu = ({ removeOrganizationMember, updateOrganizationMemberRole, toggleOrganizationMemberSeat, - } = useOrganization(router.query.orgId as string); + } = useOrganization(router.query.orgId as string, { + includeMembers: true, + }); const { user, role, seatType } = member || {}; @@ -324,7 +326,9 @@ const Page = (): ReactElement => { isOwner, leaveOrganization, isLeavingOrganization, - } = useOrganization(query.orgId as string); + } = useOrganization(query.orgId as string, { + includeMembers: true, + }); const onLeaveClick = async () => { const options: PromptOptions = { From d78dc171c379836908b8ae908c0d60b4f62214ee Mon Sep 17 00:00:00 2001 From: AmarTrebinjac Date: Mon, 23 Feb 2026 10:54:23 +0000 Subject: [PATCH 2/6] feat(shared): add skip button and swipe-up to hot take modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a skip feature to the Hot Takes modal that lets users dismiss a hot take without casting a vote. The skip button (😐) sits between the existing cold/hot buttons and swiping up on the card triggers the same behavior. The card flies upward with a fade animation and a "SKIP 😐" badge appears during the gesture. A new SkipHotTake log event is tracked for analytics. Co-Authored-By: Claude Opus 4.6 --- .../modals/hotTakes/HotAndColdModal.tsx | 197 +++++++++++++----- packages/shared/src/lib/log.ts | 1 + 2 files changed, 151 insertions(+), 47 deletions(-) diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx index 6476df6aff..66c231eaf8 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx @@ -30,6 +30,8 @@ const BUTTON_DISMISS_ANIMATION_MS = 620; const DISMISS_FLY_DISTANCE = 760; const BUTTON_DISMISS_FLY_DISTANCE = 620; const BUTTON_FLY_KICK_DELAY_MS = 42; +const SKIP_DISMISS_ANIMATION_MS = 520; +const SKIP_DISMISS_FLY_DISTANCE = 600; const EFFECT_KEYFRAMES = ` @keyframes hotTakeFlame { @@ -171,6 +173,7 @@ const HotTakeCard = ({ isTop, offset, swipeDelta, + skipDeltaY = 0, isDismissAnimating, isDragging, dismissDurationMs, @@ -179,21 +182,30 @@ const HotTakeCard = ({ isTop: boolean; offset: number; swipeDelta: number; + skipDeltaY?: number; isDismissAnimating: boolean; isDragging: boolean; dismissDurationMs: number; }): ReactElement => { + const isSkipAnimating = isTop && isDismissAnimating && skipDeltaY !== 0; const rotation = isTop ? Math.max(Math.min(swipeDelta * 0.08, 18), -18) : 0; const translateX = isTop ? swipeDelta : 0; const stackScale = isTop ? 1 : 1 - offset * 0.05; const translateY = isTop ? 0 : offset * 8; - const dismissProgress = - isTop && isDismissAnimating - ? Math.min(Math.abs(swipeDelta) / DISMISS_FLY_DISTANCE, 1) - : 0; + const getDismissProgress = (): number => { + if (!isTop || !isDismissAnimating) { + return 0; + } + if (isSkipAnimating) { + return Math.min(Math.abs(skipDeltaY) / SKIP_DISMISS_FLY_DISTANCE, 1); + } + return Math.min(Math.abs(swipeDelta) / DISMISS_FLY_DISTANCE, 1); + }; + const dismissProgress = getDismissProgress(); const scale = isTop ? 1 - dismissProgress * 0.06 : stackScale; const dismissLift = isTop ? dismissProgress * -22 : 0; - const translateYWithOutro = translateY + dismissLift; + const translateYWithOutro = + translateY + dismissLift + (isTop ? skipDeltaY : 0); const intensity = isTop ? Math.min(Math.abs(swipeDelta) / SWIPE_THRESHOLD, 1) @@ -468,6 +480,21 @@ const HotTakeCard = ({ )} + {isTop && isSkipAnimating && ( +
+ SKIP 😐 +
+ )} +
{hotTake.emoji} @@ -607,6 +634,8 @@ const HotAndColdModal = ({ const animatingTakeIdRef = useRef(null); const flyTimerRef = useRef | null>(null); const dismissTimerRef = useRef | null>(null); + const [skipDelta, setSkipDelta] = useState(0); + const swipeDeltaYRef = useRef(0); useEffect(() => { animatingTakeIdRef.current = animatingTakeId; @@ -616,6 +645,8 @@ const HotAndColdModal = ({ if (!isAnimating) { setSwipeDelta(0); swipeDeltaRef.current = 0; + setSkipDelta(0); + swipeDeltaYRef.current = 0; setIsDragging(false); } }, [currentTake?.id, isAnimating]); @@ -631,12 +662,18 @@ const HotAndColdModal = ({ }; }, []); - const handleDismiss = useCallback( - (direction: 'left' | 'right', source: 'swipe' | 'button' = 'swipe') => { - if (!currentTake || isAnimating) { - return; - } - + const startDismissAnimation = useCallback( + ({ + takeId, + durationMs, + flyDelayMs, + onFly, + }: { + takeId: string; + durationMs: number; + flyDelayMs: number; + onFly: () => void; + }) => { if (flyTimerRef.current) { clearTimeout(flyTimerRef.current); } @@ -644,18 +681,50 @@ const HotAndColdModal = ({ clearTimeout(dismissTimerRef.current); } - const isButtonDismiss = source === 'button'; - const currentDismissDurationMs = - source === 'button' - ? BUTTON_DISMISS_ANIMATION_MS - : DISMISS_ANIMATION_MS; - const flyKickDelayMs = isButtonDismiss ? BUTTON_FLY_KICK_DELAY_MS : 0; - - animatingTakeIdRef.current = currentTake.id; - setAnimatingTakeId(currentTake.id); - setDismissDurationMs(currentDismissDurationMs); + animatingTakeIdRef.current = takeId; + setAnimatingTakeId(takeId); + setDismissDurationMs(durationMs); setIsAnimating(true); setIsDragging(false); + + flyTimerRef.current = setTimeout(() => { + if (animatingTakeIdRef.current !== takeId) { + flyTimerRef.current = null; + return; + } + onFly(); + flyTimerRef.current = null; + }, flyDelayMs); + + dismissTimerRef.current = setTimeout(() => { + if (animatingTakeIdRef.current !== takeId) { + dismissTimerRef.current = null; + return; + } + setSwipeDelta(0); + swipeDeltaRef.current = 0; + setSkipDelta(0); + swipeDeltaYRef.current = 0; + animatingTakeIdRef.current = null; + setAnimatingTakeId(null); + dismissCurrent(); + setIsAnimating(false); + dismissTimerRef.current = null; + }, durationMs); + }, + [dismissCurrent], + ); + + const handleDismiss = useCallback( + (direction: 'left' | 'right', source: 'swipe' | 'button' = 'swipe') => { + if (!currentTake || isAnimating) { + return; + } + + const isButtonSource = source === 'button'; + const durationMs = isButtonSource + ? BUTTON_DISMISS_ANIMATION_MS + : DISMISS_ANIMATION_MS; const vote = direction === 'right' ? 'hot' : 'cold'; logEvent({ @@ -676,10 +745,9 @@ const HotAndColdModal = ({ }); } - // Start from current drag position, then finish the swipe with momentum. let initialPush: number; let flyDistance: number; - if (source === 'button') { + if (isButtonSource) { initialPush = direction === 'right' ? SWIPE_THRESHOLD * 0.45 @@ -697,34 +765,18 @@ const HotAndColdModal = ({ direction === 'right' ? DISMISS_FLY_DISTANCE : -DISMISS_FLY_DISTANCE; } setSwipeDelta(initialPush); - const dismissingTakeId = currentTake.id; - flyTimerRef.current = setTimeout(() => { - if (animatingTakeIdRef.current !== dismissingTakeId) { - flyTimerRef.current = null; - return; - } - setSwipeDelta(flyDistance); - flyTimerRef.current = null; - }, flyKickDelayMs); - dismissTimerRef.current = setTimeout(() => { - if (animatingTakeIdRef.current !== dismissingTakeId) { - dismissTimerRef.current = null; - return; - } - setSwipeDelta(0); - swipeDeltaRef.current = 0; - animatingTakeIdRef.current = null; - setAnimatingTakeId(null); - dismissCurrent(); - setIsAnimating(false); - dismissTimerRef.current = null; - }, currentDismissDurationMs); + startDismissAnimation({ + takeId: currentTake.id, + durationMs, + flyDelayMs: isButtonSource ? BUTTON_FLY_KICK_DELAY_MS : 0, + onFly: () => setSwipeDelta(flyDistance), + }); }, [ currentTake, isAnimating, - dismissCurrent, + startDismissAnimation, toggleDownvote, toggleUpvote, logEvent, @@ -732,10 +784,32 @@ const HotAndColdModal = ({ ], ); + const handleSkip = useCallback( + (source: 'swipe' | 'button' = 'button') => { + if (!currentTake || isAnimating) { + return; + } + + logEvent({ + event_name: LogEvent.SkipHotTake, + target_id: currentTake.id, + }); + + startDismissAnimation({ + takeId: currentTake.id, + durationMs: SKIP_DISMISS_ANIMATION_MS, + flyDelayMs: source === 'button' ? BUTTON_FLY_KICK_DELAY_MS : 0, + onFly: () => setSkipDelta(-SKIP_DISMISS_FLY_DISTANCE), + }); + }, + [currentTake, isAnimating, startDismissAnimation, logEvent], + ); + const isCurrentTakeAnimating = !!currentTake && isAnimating && animatingTakeId === currentTake.id; const cardSwipeDelta = isAnimating && !isCurrentTakeAnimating ? 0 : swipeDelta; + const cardSkipDelta = isAnimating && !isCurrentTakeAnimating ? 0 : skipDelta; const handleSwiped = (direction: 'left' | 'right') => { setIsDragging(false); @@ -744,6 +818,7 @@ const HotAndColdModal = ({ } else { setSwipeDelta(0); swipeDeltaRef.current = 0; + swipeDeltaYRef.current = 0; } }; @@ -753,10 +828,24 @@ const HotAndColdModal = ({ setIsDragging(true); setSwipeDelta(e.deltaX); swipeDeltaRef.current = e.deltaX; + swipeDeltaYRef.current = e.deltaY; } }, onSwipedLeft: () => handleSwiped('left'), onSwipedRight: () => handleSwiped('right'), + onSwipedUp: () => { + setIsDragging(false); + if ( + swipeDeltaYRef.current < 0 && + Math.abs(swipeDeltaYRef.current) > SWIPE_THRESHOLD + ) { + handleSkip('swipe'); + } else { + setSwipeDelta(0); + swipeDeltaRef.current = 0; + swipeDeltaYRef.current = 0; + } + }, trackMouse: true, preventScrollOnSwipe: true, touchEventOptions: { passive: false }, @@ -806,13 +895,14 @@ const HotAndColdModal = ({ isTop offset={0} swipeDelta={cardSwipeDelta} + skipDeltaY={cardSkipDelta} isDismissAnimating={isCurrentTakeAnimating} isDragging={isDragging} dismissDurationMs={dismissDurationMs} />
-
+
)} + {isSkipVisualActive && ( +
+
+
+ {SLEEP_ZS.map((sleepZ) => ( + + Z + + ))} + {SLEEP_BUBBLES.map((bubble) => ( +
+ ))} +
+ )} + {isTop && swipeDirection && (
)} - {isTop && isSkipAnimating && ( + {isSkipVisualActive && (
- SKIP 😐 + SKIP 😴
)} From 5277de1b4e65d06316dc912f5b9efba04a349d0b Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Mon, 23 Feb 2026 14:27:05 +0100 Subject: [PATCH 5/6] darker color --- .../src/components/modals/hotTakes/HotAndColdModal.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx index 91093c0960..3a4a06b0f8 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx @@ -33,6 +33,7 @@ const BUTTON_FLY_KICK_DELAY_MS = 42; const SKIP_DISMISS_ANIMATION_MS = 520; const SKIP_DISMISS_FLY_DISTANCE = 600; const SKIP_DRAG_ELASTICITY_FACTOR = 0.3; +const COLD_ACCENT_COLOR = '#123a88'; const getElasticDelta = (delta: number): number => { const absoluteDelta = Math.abs(delta); @@ -322,7 +323,7 @@ const HotTakeCard = ({ const accentColor = swipeDirection === 'right' ? 'var(--theme-accent-ketchup-default)' - : 'var(--theme-accent-blueCheese-default)'; + : COLD_ACCENT_COLOR; const skipAccentColor = 'var(--theme-accent-blueCheese-default)'; let activeBorderColor; if (swipeDirection) { @@ -681,11 +682,13 @@ const HotTakeCard = ({ 'z-20 absolute left-1/2 top-4 -translate-x-1/2 rounded-10 px-4 py-1 font-bold typo-title3', swipeDirection === 'right' ? 'bg-accent-ketchup-default text-white' - : 'bg-accent-blueCheese-default text-white', + : 'text-white', )} style={{ opacity: effectIntensity, animation: 'hotTakeBadgePulse 0.18s ease-out', + backgroundColor: + swipeDirection === 'right' ? undefined : COLD_ACCENT_COLOR, boxShadow: `0 6px ${12 + effectIntensity * 10}px rgba(0,0,0,${ 0.1 + effectIntensity * 0.18 })`, From 0e2fe17080ceb59061bd45170516d4a4a82b7598 Mon Sep 17 00:00:00 2001 From: Amar Trebinjac Date: Mon, 23 Feb 2026 14:36:19 +0100 Subject: [PATCH 6/6] cast vote on skip --- .../modals/hotTakes/HotAndColdModal.spec.tsx | 36 +++++++++++++++++++ .../modals/hotTakes/HotAndColdModal.tsx | 12 +++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx index f54decdc70..d6da2501e1 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.spec.tsx @@ -57,6 +57,7 @@ const renderComponent = (onRequestClose = jest.fn()) => { describe('HotAndColdModal', () => { const toggleUpvote = jest.fn(); const toggleDownvote = jest.fn(); + const cancelHotTakeVote = jest.fn(); const dismissCurrent = jest.fn(); const logEvent = jest.fn(); @@ -65,6 +66,7 @@ describe('HotAndColdModal', () => { mockedUseVoteHotTake.mockReturnValue({ toggleUpvote, toggleDownvote, + cancelHotTakeVote, }); mockedUseLogContext.mockReturnValue({ logEvent, @@ -176,6 +178,40 @@ describe('HotAndColdModal', () => { expect(dismissCurrent).toHaveBeenCalledTimes(1); }); + it('should trigger neutral vote flow for skip action', () => { + jest.useFakeTimers(); + const currentTake = createHotTake('skip-take'); + + mockedUseDiscoverHotTakes.mockReturnValue({ + hotTakes: [currentTake], + currentTake, + nextTake: null, + isEmpty: false, + isLoading: false, + dismissCurrent, + }); + + renderComponent(); + + fireEvent.click(screen.getByRole('button', { name: 'Skip hot take' })); + + expect(cancelHotTakeVote).toHaveBeenCalledWith({ id: currentTake.id }); + expect(toggleUpvote).not.toHaveBeenCalled(); + expect(toggleDownvote).not.toHaveBeenCalled(); + expect(logEvent).toHaveBeenCalledWith( + expect.objectContaining({ + event_name: LogEvent.SkipHotTake, + target_id: currentTake.id, + }), + ); + + act(() => { + jest.runAllTimers(); + }); + + expect(dismissCurrent).toHaveBeenCalledTimes(1); + }); + it('should render empty state share button and close modal on click', () => { mockedUseDiscoverHotTakes.mockReturnValue({ hotTakes: [], diff --git a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx index 3a4a06b0f8..8c1dc01aa3 100644 --- a/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx +++ b/packages/shared/src/components/modals/hotTakes/HotAndColdModal.tsx @@ -839,7 +839,7 @@ const HotAndColdModal = ({ }: ModalProps): ReactElement => { const { currentTake, nextTake, isEmpty, isLoading, dismissCurrent } = useDiscoverHotTakes(); - const { toggleUpvote, toggleDownvote } = useVoteHotTake(); + const { toggleUpvote, toggleDownvote, cancelHotTakeVote } = useVoteHotTake(); const { logEvent } = useLogContext(); const { user } = useAuthContext(); const [swipeDelta, setSwipeDelta] = useState(0); @@ -1013,6 +1013,8 @@ const HotAndColdModal = ({ target_id: currentTake.id, }); + cancelHotTakeVote({ id: currentTake.id }); + startDismissAnimation({ takeId: currentTake.id, durationMs: SKIP_DISMISS_ANIMATION_MS, @@ -1020,7 +1022,13 @@ const HotAndColdModal = ({ onFly: () => setSkipDelta(-SKIP_DISMISS_FLY_DISTANCE), }); }, - [currentTake, isAnimating, startDismissAnimation, logEvent], + [ + cancelHotTakeVote, + currentTake, + isAnimating, + startDismissAnimation, + logEvent, + ], ); const isCurrentTakeAnimating =