Skip to content
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
19 changes: 12 additions & 7 deletions packages/shared/src/components/post/write/CreatePostButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function CreatePostButton<Tag extends AllowedTags>({
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);
Expand Down Expand Up @@ -106,7 +106,15 @@ export function CreatePostButton<Tag extends AllowedTags>({
});
};

const shouldUseSmartComposer = isSmartComposerEnabled && isLaptop && !onClick;
const onCreatePostClick = (
event: React.MouseEvent<AllowedElements, MouseEvent>,
) => {
if (!isLaptop || !evaluateSmartComposer()) {
return;
}

openSmartComposer(event);
};

const buttonProps: {
tag?: AllowedTags;
Expand All @@ -115,13 +123,10 @@ export function CreatePostButton<Tag extends AllowedTags>({
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);
Expand Down
5 changes: 2 additions & 3 deletions packages/shared/src/components/squads/SharePostBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ function SharePostBar({
const [url, setUrl] = useState<string>('');
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) {
Expand All @@ -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: {
Expand Down
56 changes: 26 additions & 30 deletions packages/shared/src/components/squads/SquadPageHeader.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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<HTMLAnchorElement>) => {
if (!isLaptop || !evaluateSmartComposer()) {
return;
}

event.preventDefault();
openModal({
type: LazyModal.SmartComposer,
props: { initialSquadHandle: squad.handle },
});
},
[evaluateSmartComposer, isLaptop, openModal, squad.handle],
);

return (
<FlexCol
Expand Down Expand Up @@ -259,32 +271,16 @@ export function SquadPageHeader({
<span className="absolute -left-6 flex h-px w-[calc(100%+3rem)] bg-border-subtlest-tertiary tablet:hidden" />
<span className="z-0 bg-background-default px-4">or</span>
</FlexCentered>
{shouldUseSmartComposer ? (
<Button
type="button"
onClick={() =>
openModal({
type: LazyModal.SmartComposer,
props: { initialSquadHandle: squad.handle },
})
}
variant={ButtonVariant.Primary}
color={ButtonColor.Cabbage}
className="w-full tablet:w-auto"
>
New post
</Button>
) : (
<Button
tag="a"
href={`${link.post.create}?sid=${squad.handle}`}
variant={ButtonVariant.Primary}
color={ButtonColor.Cabbage}
className="w-full tablet:w-auto"
>
New post
</Button>
)}
<Button
tag="a"
href={`${link.post.create}?sid=${squad.handle}`}
onClick={onNewPostClick}
variant={ButtonVariant.Primary}
color={ButtonColor.Cabbage}
className="w-full tablet:w-auto"
>
New post
</Button>
<Divider />
</>
)}
Expand Down
11 changes: 5 additions & 6 deletions packages/shared/src/features/posts/PostOptionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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: <MenuIcon Icon={EditIcon} />,
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 },
Expand Down
Original file line number Diff line number Diff line change
@@ -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<unknown>),
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) => (
<QueryClientProvider client={client}>{children}</QueryClientProvider>
);

beforeEach(() => {
client = new QueryClient();
jest.clearAllMocks();

checkHasCompleted.mockReturnValue(true);
jest.mocked(useActions).mockReturnValue({
isActionsFetched: true,
checkHasCompleted,
completeAction,
} as unknown as ReturnType<typeof useActions>);
jest.mocked(usePrompt).mockReturnValue({
showPrompt,
} as unknown as ReturnType<typeof usePrompt>);
jest.mocked(useLogContext).mockReturnValue({
logEvent,
} as unknown as ReturnType<typeof useLogContext>);
});

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);
});
});
46 changes: 44 additions & 2 deletions packages/shared/src/features/squads/hooks/useMultipleSourcePost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,12 +28,39 @@ interface UseMultipleSourcePost {
) => Promise<null | CreationInMultipleSourcesResult>;
}

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(
() =>
Expand All @@ -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,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/shared/src/graphql/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ export const CREATE_POST_IN_MULTIPLE_SOURCES = gql`

export interface CreatePostInMultipleSourcesArgs
extends Partial<CreatePostProps>,
Pick<CreatePollPostProps, 'options' | 'duration'> {
Partial<Pick<CreatePollPostProps, 'options' | 'duration'>> {
commentary?: string;
externalLink?: string;
imageUrl?: string;
Expand Down
Loading
Loading