From b6de91bf454c88154a97d84808a5e24d2d279656 Mon Sep 17 00:00:00 2001 From: Chris Bongers Date: Tue, 2 Jun 2026 15:30:21 +0200 Subject: [PATCH] fix: defer smart composer enrollment --- .../post/write/CreatePostButton.tsx | 19 ++-- .../src/components/squads/SharePostBar.tsx | 5 +- .../src/components/squads/SquadPageHeader.tsx | 56 +++++----- .../src/features/posts/PostOptionButton.tsx | 11 +- .../hooks/useMultipleSourcePost.spec.tsx | 101 ++++++++++++++++++ .../squads/hooks/useMultipleSourcePost.ts | 46 +++++++- packages/shared/src/graphql/posts.ts | 2 +- .../src/hooks/post/useLogPostCreated.ts | 43 ++++++++ .../src/hooks/post/useSmartComposer.spec.tsx | 51 +++++++++ .../shared/src/hooks/post/useSmartComposer.ts | 25 +++-- .../src/hooks/squads/usePostToSquad.spec.tsx | 31 +++++- .../src/hooks/squads/usePostToSquad.tsx | 49 ++++++++- packages/shared/src/lib/log.ts | 1 + 13 files changed, 381 insertions(+), 59 deletions(-) create mode 100644 packages/shared/src/features/squads/hooks/useMultipleSourcePost.spec.tsx create mode 100644 packages/shared/src/hooks/post/useLogPostCreated.ts create mode 100644 packages/shared/src/hooks/post/useSmartComposer.spec.tsx diff --git a/packages/shared/src/components/post/write/CreatePostButton.tsx b/packages/shared/src/components/post/write/CreatePostButton.tsx index 91e4edbea6e..bab2615e513 100644 --- a/packages/shared/src/components/post/write/CreatePostButton.tsx +++ b/packages/shared/src/components/post/write/CreatePostButton.tsx @@ -44,7 +44,7 @@ export function CreatePostButton({ const { user, squads } = useAuthContext(); const { route, query } = useRouter(); const { openModal } = useLazyModal(); - const isSmartComposerEnabled = useSmartComposer(); + const { evaluateSmartComposer } = useSmartComposer(); const isTablet = useViewSize(ViewSize.Tablet); const isLaptop = useViewSize(ViewSize.Laptop); const isLaptopL = useViewSize(ViewSize.LaptopL); @@ -106,7 +106,15 @@ export function CreatePostButton({ }); }; - const shouldUseSmartComposer = isSmartComposerEnabled && isLaptop && !onClick; + const onCreatePostClick = ( + event: React.MouseEvent, + ) => { + if (!isLaptop || !evaluateSmartComposer()) { + return; + } + + openSmartComposer(event); + }; const buttonProps: { tag?: AllowedTags; @@ -115,13 +123,10 @@ export function CreatePostButton({ if (onClick) { return { onClick }; } - if (shouldUseSmartComposer) { - return { onClick: openSmartComposer }; - } - return { tag: 'a' }; + return { tag: 'a', onClick: onCreatePostClick }; })(); - const shouldUseLink = !onClick && !shouldUseSmartComposer; + const shouldUseLink = !onClick; const shouldShowAsCompact = compact !== false && ((isLaptop && !isLaptopL) || compact); diff --git a/packages/shared/src/components/squads/SharePostBar.tsx b/packages/shared/src/components/squads/SharePostBar.tsx index 60bbbc4f2a3..60e55139ff8 100644 --- a/packages/shared/src/components/squads/SharePostBar.tsx +++ b/packages/shared/src/components/squads/SharePostBar.tsx @@ -35,8 +35,7 @@ function SharePostBar({ const [url, setUrl] = useState(''); const isMobile = useViewSize(ViewSize.MobileL); const isLaptop = useViewSize(ViewSize.Laptop); - const isSmartComposerEnabled = useSmartComposer(); - const shouldUseSmartComposer = isSmartComposerEnabled && isLaptop; + const { evaluateSmartComposer } = useSmartComposer(); const [urlFocused, toggleUrlFocus] = useState(false); const onSharedSuccessfully = () => { if (inputRef.current) { @@ -48,7 +47,7 @@ function SharePostBar({ const shouldRenderReadingHistory = !urlFocused && url.length === 0; const onOpenCreatePost = (preview: ExternalLinkPreview, link?: string) => { - if (shouldUseSmartComposer) { + if (isLaptop && evaluateSmartComposer()) { openModal({ type: LazyModal.SmartComposer, props: { diff --git a/packages/shared/src/components/squads/SquadPageHeader.tsx b/packages/shared/src/components/squads/SquadPageHeader.tsx index 741bcb43a6d..bdbb0e2e7eb 100644 --- a/packages/shared/src/components/squads/SquadPageHeader.tsx +++ b/packages/shared/src/components/squads/SquadPageHeader.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react'; -import React from 'react'; +import React, { useCallback } from 'react'; import classNames from 'classnames'; import type { BasicSourceMember, Squad } from '../../graphql/sources'; import { SourceMemberRole, SourcePermissions } from '../../graphql/sources'; @@ -56,11 +56,9 @@ export function SquadPageHeader({ hideHeaderBar = false, }: SquadPageHeaderProps): ReactElement { const { openModal } = useLazyModal(); - const isSmartComposerEnabled = useSmartComposer(); + const { evaluateSmartComposer } = useSmartComposer(); const isLaptop = useViewSize(ViewSize.Laptop); const allowedToPost = verifyPermission(squad, SourcePermissions.Post); - const shouldUseSmartComposer = - isSmartComposerEnabled && isLaptop && allowedToPost; const { category } = squad; const squadId = squad.id ?? ''; const isSquadMember = !!squad.currentMember; @@ -75,6 +73,20 @@ export function SquadPageHeader({ const listMax = isMobile ? MAX_VISIBLE_PRIVILEGED_MEMBERS_MOBILE : MAX_VISIBLE_PRIVILEGED_MEMBERS_LAPTOP; + const onNewPostClick = useCallback( + (event: React.MouseEvent) => { + if (!isLaptop || !evaluateSmartComposer()) { + return; + } + + event.preventDefault(); + openModal({ + type: LazyModal.SmartComposer, + props: { initialSquadHandle: squad.handle }, + }); + }, + [evaluateSmartComposer, isLaptop, openModal, squad.handle], + ); return ( or - {shouldUseSmartComposer ? ( - - ) : ( - - )} + )} diff --git a/packages/shared/src/features/posts/PostOptionButton.tsx b/packages/shared/src/features/posts/PostOptionButton.tsx index 3c0258c5842..932746f8a44 100644 --- a/packages/shared/src/features/posts/PostOptionButton.tsx +++ b/packages/shared/src/features/posts/PostOptionButton.tsx @@ -198,7 +198,7 @@ const PostOptionButtonContent = ({ const { follow, unfollow, unblock, block } = useContentPreference(); const { openModal } = useLazyModal(); const { showPrompt } = usePrompt(); - const isSmartComposerEnabled = useSmartComposer(); + const { evaluateSmartComposer } = useSmartComposer(); const isLaptop = useViewSize(ViewSize.Laptop); const { isCustomDefaultFeed, defaultFeedId } = useCustomDefaultFeed(); const { canBoost } = useCanBoostPost(post); @@ -802,15 +802,14 @@ const PostOptionButtonContent = ({ user?.id && post?.author?.id === user?.id ) { - const canUseSmartComposer = - isSmartComposerEnabled && - isLaptop && - (post.type === PostType.Freeform || post.type === PostType.Welcome); postOptions.push({ icon: , label: 'Edit post', action: () => { - if (canUseSmartComposer) { + const canUseSmartComposer = + isLaptop && + (post.type === PostType.Freeform || post.type === PostType.Welcome); + if (canUseSmartComposer && evaluateSmartComposer()) { openModal({ type: LazyModal.SmartComposer, props: { editPost: post }, diff --git a/packages/shared/src/features/squads/hooks/useMultipleSourcePost.spec.tsx b/packages/shared/src/features/squads/hooks/useMultipleSourcePost.spec.tsx new file mode 100644 index 00000000000..5d710b26f1f --- /dev/null +++ b/packages/shared/src/features/squads/hooks/useMultipleSourcePost.spec.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { act, renderHook } from '@testing-library/react'; +import { createPostInMultipleSources, PostType } from '../../../graphql/posts'; +import { useActions } from '../../../hooks'; +import { usePrompt } from '../../../hooks/usePrompt'; +import { useLogContext } from '../../../contexts/LogContext'; +import { LogEvent } from '../../../lib/log'; +import { useMultipleSourcePost } from './useMultipleSourcePost'; + +jest.mock('../../../graphql/posts', () => ({ + ...(jest.requireActual('../../../graphql/posts') as Iterable), + createPostInMultipleSources: jest.fn(), +})); + +jest.mock('../../../hooks', () => ({ + useActions: jest.fn(), +})); + +jest.mock('../../../hooks/usePrompt', () => ({ + usePrompt: jest.fn(), +})); + +jest.mock('../../../contexts/LogContext', () => ({ + useLogContext: jest.fn(), +})); + +describe('useMultipleSourcePost', () => { + const checkHasCompleted = jest.fn(); + const completeAction = jest.fn(); + const showPrompt = jest.fn(); + const logEvent = jest.fn(); + const onSuccess = jest.fn(); + let client: QueryClient; + + const wrapper = ({ children }: React.PropsWithChildren) => ( + {children} + ); + + beforeEach(() => { + client = new QueryClient(); + jest.clearAllMocks(); + + checkHasCompleted.mockReturnValue(true); + jest.mocked(useActions).mockReturnValue({ + isActionsFetched: true, + checkHasCompleted, + completeAction, + } as unknown as ReturnType); + jest.mocked(usePrompt).mockReturnValue({ + showPrompt, + } as unknown as ReturnType); + jest.mocked(useLogContext).mockReturnValue({ + logEvent, + } as unknown as ReturnType); + }); + + it('logs the post creation outcome for multi-source creation', async () => { + jest.mocked(createPostInMultipleSources).mockResolvedValue([ + { + id: 'post-1', + sourceId: 'source-1', + type: 'post', + slug: 'post-slug', + }, + { + id: 'moderation-1', + sourceId: 'source-2', + type: 'moderationItem', + }, + ]); + + const { result } = renderHook( + () => + useMultipleSourcePost({ + onSuccess, + }), + { wrapper }, + ); + + await act(async () => { + await result.current.onCreate({ + sourceIds: ['source-1', 'source-2'], + title: 'Post title', + content: 'Post body', + }); + }); + + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.CreatePost, + target_id: 'post-1', + target_type: 'post', + extra: JSON.stringify({ + post_type: PostType.Freeform, + source_count: 2, + moderation_count: 1, + }), + }); + expect(onSuccess).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/shared/src/features/squads/hooks/useMultipleSourcePost.ts b/packages/shared/src/features/squads/hooks/useMultipleSourcePost.ts index 1e60e8edd92..0ca400ec8ea 100644 --- a/packages/shared/src/features/squads/hooks/useMultipleSourcePost.ts +++ b/packages/shared/src/features/squads/hooks/useMultipleSourcePost.ts @@ -4,12 +4,13 @@ import type { CreatePostInMultipleSourcesArgs, CreatePostInMultipleSourcesResponse, } from '../../../graphql/posts'; -import { createPostInMultipleSources } from '../../../graphql/posts'; +import { createPostInMultipleSources, PostType } from '../../../graphql/posts'; import { useActions } from '../../../hooks'; import { ActionType } from '../../../graphql/actions'; import { usePrompt } from '../../../hooks/usePrompt'; import type { ApiErrorResult } from '../../../graphql/common'; import { labels } from '../../../lib'; +import { useLogPostCreated } from '../../../hooks/post/useLogPostCreated'; interface UseMultipleSourcePostProps { onError?: (error: ApiErrorResult) => void; @@ -27,12 +28,39 @@ interface UseMultipleSourcePost { ) => Promise; } +const getPostType = (args: CreatePostInMultipleSourcesArgs): PostType => { + if (args.options?.length) { + return PostType.Poll; + } + + if (args.externalLink || args.sharedPostId) { + return PostType.Share; + } + + return PostType.Freeform; +}; + +const getTargetType = ( + item: CreatePostInMultipleSourcesResponse[number] | undefined, +): string | undefined => { + if (!item) { + return undefined; + } + + if (item.type === 'moderationItem') { + return 'moderation_item'; + } + + return 'post'; +}; + export const useMultipleSourcePost = ({ onSuccess, onError, }: UseMultipleSourcePostProps): UseMultipleSourcePost => { const { isActionsFetched, checkHasCompleted, completeAction } = useActions(); const { showPrompt } = usePrompt(); + const logPostCreated = useLogPostCreated(); const hasSeenOpenSquadWarning = useMemo( () => @@ -43,7 +71,21 @@ export const useMultipleSourcePost = ({ const { mutateAsync: requestPostCreation, isPending } = useMutation({ mutationFn: createPostInMultipleSources, - onSuccess, + onSuccess: (data, args) => { + const firstItem = data[0]; + const moderationCount = data.filter( + (item) => item.type === 'moderationItem', + ).length; + + logPostCreated({ + postId: firstItem?.id, + postType: getPostType(args), + sourceCount: args.sourceIds.length, + moderationCount, + targetType: getTargetType(firstItem), + }); + onSuccess?.(data); + }, onError, }); diff --git a/packages/shared/src/graphql/posts.ts b/packages/shared/src/graphql/posts.ts index 9b8e8d9ab87..49740cbafa6 100644 --- a/packages/shared/src/graphql/posts.ts +++ b/packages/shared/src/graphql/posts.ts @@ -1015,7 +1015,7 @@ export const CREATE_POST_IN_MULTIPLE_SOURCES = gql` export interface CreatePostInMultipleSourcesArgs extends Partial, - Pick { + Partial> { commentary?: string; externalLink?: string; imageUrl?: string; diff --git a/packages/shared/src/hooks/post/useLogPostCreated.ts b/packages/shared/src/hooks/post/useLogPostCreated.ts new file mode 100644 index 00000000000..e960765cfe1 --- /dev/null +++ b/packages/shared/src/hooks/post/useLogPostCreated.ts @@ -0,0 +1,43 @@ +import { useCallback } from 'react'; +import { useLogContext } from '../../contexts/LogContext'; +import { LogEvent } from '../../lib/log'; + +interface LogPostCreatedParams { + moderationCount?: number; + postId?: string; + postType?: string; + sourceCount?: number; + targetType?: string; +} + +export const useLogPostCreated = (): (( + params?: LogPostCreatedParams, +) => void) => { + const { logEvent } = useLogContext(); + + return useCallback( + ({ + moderationCount, + postId, + postType, + sourceCount, + targetType, + }: LogPostCreatedParams = {}) => { + const extra = { + ...(postType ? { post_type: postType } : {}), + ...(sourceCount != null ? { source_count: sourceCount } : {}), + ...(moderationCount != null + ? { moderation_count: moderationCount } + : {}), + }; + + logEvent({ + event_name: LogEvent.CreatePost, + target_id: postId, + target_type: targetType, + extra: Object.keys(extra).length ? JSON.stringify(extra) : undefined, + }); + }, + [logEvent], + ); +}; diff --git a/packages/shared/src/hooks/post/useSmartComposer.spec.tsx b/packages/shared/src/hooks/post/useSmartComposer.spec.tsx new file mode 100644 index 00000000000..42e3c62541f --- /dev/null +++ b/packages/shared/src/hooks/post/useSmartComposer.spec.tsx @@ -0,0 +1,51 @@ +import { renderHook } from '@testing-library/react'; +import { useAuthContext } from '../../contexts/AuthContext'; +import { useFeaturesReadyContext } from '../../components/GrowthBookProvider'; +import { featureSmartComposer } from '../../lib/featureManagement'; +import { useSmartComposer } from './useSmartComposer'; + +jest.mock('../../contexts/AuthContext', () => ({ + useAuthContext: jest.fn(), +})); + +jest.mock('../../components/GrowthBookProvider', () => ({ + useFeaturesReadyContext: jest.fn(), +})); + +describe('useSmartComposer', () => { + const getFeatureValue = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + jest.mocked(useAuthContext).mockReturnValue({ + user: { id: 'user-1' }, + isAuthReady: true, + } as unknown as ReturnType); + jest.mocked(useFeaturesReadyContext).mockReturnValue({ + ready: true, + getFeatureValue, + }); + }); + + it('evaluates the experiment only when requested', () => { + getFeatureValue.mockReturnValue(true); + + const { result } = renderHook(() => useSmartComposer()); + + expect(getFeatureValue).not.toHaveBeenCalled(); + expect(result.current.evaluateSmartComposer()).toBe(true); + expect(getFeatureValue).toHaveBeenCalledWith(featureSmartComposer); + }); + + it('returns the default value without enrolling when features are not ready', () => { + jest.mocked(useFeaturesReadyContext).mockReturnValue({ + ready: false, + getFeatureValue, + }); + + const { result } = renderHook(() => useSmartComposer()); + + expect(result.current.evaluateSmartComposer()).toBe(false); + expect(getFeatureValue).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/shared/src/hooks/post/useSmartComposer.ts b/packages/shared/src/hooks/post/useSmartComposer.ts index acd6c28ccff..604ea5a8131 100644 --- a/packages/shared/src/hooks/post/useSmartComposer.ts +++ b/packages/shared/src/hooks/post/useSmartComposer.ts @@ -1,12 +1,23 @@ +import { useCallback } from 'react'; +import { useFeaturesReadyContext } from '../../components/GrowthBookProvider'; import { useAuthContext } from '../../contexts/AuthContext'; -import { useConditionalFeature } from '../useConditionalFeature'; import { featureSmartComposer } from '../../lib/featureManagement'; -export const useSmartComposer = (): boolean => { +interface UseSmartComposer { + evaluateSmartComposer: () => boolean; +} + +export const useSmartComposer = (): UseSmartComposer => { const { user, isAuthReady } = useAuthContext(); - const { value } = useConditionalFeature({ - feature: featureSmartComposer, - shouldEvaluate: isAuthReady && !!user, - }); - return !!value; + const { ready, getFeatureValue } = useFeaturesReadyContext(); + + const evaluateSmartComposer = useCallback(() => { + if (!isAuthReady || !user || !ready) { + return featureSmartComposer.defaultValue; + } + + return !!getFeatureValue(featureSmartComposer); + }, [getFeatureValue, isAuthReady, ready, user]); + + return { evaluateSmartComposer }; }; diff --git a/packages/shared/src/hooks/squads/usePostToSquad.spec.tsx b/packages/shared/src/hooks/squads/usePostToSquad.spec.tsx index da381d59dca..ce44b9d7012 100644 --- a/packages/shared/src/hooks/squads/usePostToSquad.spec.tsx +++ b/packages/shared/src/hooks/squads/usePostToSquad.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, renderHook } from '@testing-library/react'; import { usePostToSquad } from './usePostToSquad'; -import { submitExternalLink } from '../../graphql/posts'; +import { PostType, submitExternalLink } from '../../graphql/posts'; import { addPostToSquad } from '../../graphql/squads'; import { useToastNotification } from '../useToastNotification'; import { useAuthContext } from '../../contexts/AuthContext'; @@ -10,6 +10,8 @@ import { useActions } from '../useActions'; import { useRequestProtocol } from '../useRequestProtocol'; import useSourcePostModeration from '../source/useSourcePostModeration'; import useNotificationSettings from '../notifications/useNotificationSettings'; +import { useLogContext } from '../../contexts/LogContext'; +import { LogEvent } from '../../lib/log'; jest.mock('../../graphql/posts', () => ({ ...(jest.requireActual('../../graphql/posts') as Iterable), @@ -42,12 +44,17 @@ jest.mock('../source/useSourcePostModeration', () => jest.fn()); jest.mock('../notifications/useNotificationSettings', () => jest.fn()); +jest.mock('../../contexts/LogContext', () => ({ + useLogContext: jest.fn(), +})); + describe('usePostToSquad', () => { const displayToast = jest.fn(); const completeAction = jest.fn(); const requestMethod = jest.fn(); const onComplete = jest.fn(); const onPostSuccess = jest.fn(); + const logEvent = jest.fn(); let client: QueryClient; const wrapper = ({ children }: React.PropsWithChildren) => ( @@ -61,6 +68,9 @@ describe('usePostToSquad', () => { jest.mocked(useToastNotification).mockReturnValue({ displayToast, } as unknown as ReturnType); + jest.mocked(useLogContext).mockReturnValue({ + logEvent, + } as unknown as ReturnType); jest.mocked(useAuthContext).mockReturnValue({ user: { id: 'user-1' }, } as ReturnType); @@ -120,12 +130,22 @@ describe('usePostToSquad', () => { ); expect(onComplete).toHaveBeenCalledTimes(1); expect(onPostSuccess).not.toHaveBeenCalled(); + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.CreatePost, + target_id: undefined, + target_type: undefined, + extra: JSON.stringify({ + post_type: PostType.Share, + source_count: 1, + }), + }); }); it('fires onComplete alongside onPostSuccess for shared post submissions', async () => { const mutatePost = jest.fn().mockResolvedValue({ id: 'post-1', permalink: 'https://daily.dev/posts/1', + type: PostType.Share, }); jest.mocked(addPostToSquad).mockReturnValue(mutatePost); @@ -163,5 +183,14 @@ describe('usePostToSquad', () => { 'https://daily.dev/posts/1', ); expect(onComplete).toHaveBeenCalledTimes(1); + expect(logEvent).toHaveBeenCalledWith({ + event_name: LogEvent.CreatePost, + target_id: 'post-1', + target_type: 'post', + extra: JSON.stringify({ + post_type: PostType.Share, + source_count: 1, + }), + }); }); }); diff --git a/packages/shared/src/hooks/squads/usePostToSquad.tsx b/packages/shared/src/hooks/squads/usePostToSquad.tsx index 103dbc8ac9a..3333f46a3ba 100644 --- a/packages/shared/src/hooks/squads/usePostToSquad.tsx +++ b/packages/shared/src/hooks/squads/usePostToSquad.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import type { UseMutateAsyncFunction } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { BaseSyntheticEvent } from 'react'; @@ -39,6 +39,7 @@ import { moderationRequired } from '../../components/squads/utils'; import useNotificationSettings from '../notifications/useNotificationSettings'; import { ButtonSize } from '../../components/buttons/common'; import { BellIcon } from '../../components/icons'; +import { useLogPostCreated } from '../post/useLogPostCreated'; const isApiErrorResult = (error: unknown): error is ApiErrorResult => !!(error as ApiErrorResult)?.response?.errors; @@ -108,9 +109,11 @@ export const usePostToSquad = ({ }: UsePostToSquadProps = {}): UsePostToSquad => { const { toggleGroup, getGroupStatus } = useNotificationSettings(); const { displayToast } = useToastNotification(); + const logPostCreated = useLogPostCreated(); const { user } = useAuthContext(); const client = useQueryClient(); const { completeAction } = useActions(); + const moderationCreationRef = useRef(null); const [preview, setPreview] = useState( initialPreview ?? {}, ); @@ -151,7 +154,15 @@ export const usePostToSquad = ({ mutationFn: createPost, onMutate, onError: (err) => handleMutationError(err), - onSuccess: handlePostSuccess, + onSuccess: (data) => { + logPostCreated({ + postId: data.id, + postType: data.type, + sourceCount: 1, + targetType: 'post', + }); + handlePostSuccess(data); + }, }); const { mutateAsync: editPostMutation, @@ -185,6 +196,12 @@ export const usePostToSquad = ({ }, }); } + logPostCreated({ + postId: data.id, + postType: data.type, + sourceCount: 1, + targetType: 'post', + }); handlePostSuccess(data); }, onError: (err) => handleMutationError(err), @@ -213,6 +230,15 @@ export const usePostToSquad = ({ isPending: isPostModerationLoading, } = useSourcePostModeration({ onSuccess: (data) => { + if (moderationCreationRef.current) { + logPostCreated({ + postId: data.id, + postType: moderationCreationRef.current, + sourceCount: 1, + moderationCount: 1, + targetType: 'moderation_item', + }); + } completeAction(ActionType.SquadFirstPost); onSourcePostModerationSuccess?.(data); handleComplete(); @@ -220,6 +246,9 @@ export const usePostToSquad = ({ onError: () => { displayToast(DEFAULT_ERROR); }, + onSettled: () => { + moderationCreationRef.current = null; + }, }); const onEditFreeformPost = useCallback( @@ -230,6 +259,7 @@ export const usePostToSquad = ({ if (moderationRequired(squad)) { const squadId = getSquadIdOrThrow(squad); + moderationCreationRef.current = null; await onCreatePostModeration({ ...editedPost, @@ -274,6 +304,12 @@ export const usePostToSquad = ({ } = useMutation({ mutationFn: addPostToSquad(requestMethod), onSuccess: (data) => { + logPostCreated({ + postId: data.id, + postType: data.type, + sourceCount: 1, + targetType: 'post', + }); onSharedPostSuccessfully(); handlePostSuccess(data); }, @@ -301,6 +337,10 @@ export const usePostToSquad = ({ mutationFn: (params: SubmitExternalLink) => submitExternalLink(params, requestMethod), onSuccess: (_, { url }) => { + logPostCreated({ + postType: PostType.Share, + sourceCount: 1, + }); onSharedPostSuccessfully(); if (!url) { throw new Error('Missing external link url in usePostToSquad'); @@ -346,6 +386,7 @@ export const usePostToSquad = ({ if (preview.id) { if (moderationRequired(squad)) { + moderationCreationRef.current = PostType.Share; return onCreatePostModeration({ type: PostType.Share, sourceId: squadId, @@ -370,6 +411,7 @@ export const usePostToSquad = ({ } if (moderationRequired(squad)) { + moderationCreationRef.current = null; return onCreatePostModeration({ externalLink: url, title, @@ -409,6 +451,7 @@ export const usePostToSquad = ({ const squadId = getSquadIdOrThrow(squad); if (moderationRequired(squad)) { + moderationCreationRef.current = PostType.Share; return onCreatePostModeration({ postId, type: PostType.Share, @@ -432,6 +475,7 @@ export const usePostToSquad = ({ const squadId = getSquadIdOrThrow(squad); if (moderationRequired(squad)) { + moderationCreationRef.current = PostType.Freeform; await onCreatePostModeration({ ...post, sourceId: squadId, @@ -457,6 +501,7 @@ export const usePostToSquad = ({ })); if (moderationRequired(squad)) { + moderationCreationRef.current = PostType.Poll; await onCreatePostModeration({ ...post, pollOptions: orderedOpts, diff --git a/packages/shared/src/lib/log.ts b/packages/shared/src/lib/log.ts index 2af37fe4949..2b020ae1fa2 100644 --- a/packages/shared/src/lib/log.ts +++ b/packages/shared/src/lib/log.ts @@ -103,6 +103,7 @@ export enum LogEvent { OpenSmartComposer = 'open smart composer', CloseSmartComposer = 'close smart composer', SubmitSmartComposer = 'submit smart composer', + CreatePost = 'create post', SwitchComposerKind = 'switch composer kind', ToggleComposerMarkdown = 'toggle composer markdown', ToggleComposerExpand = 'toggle composer expand',