Skip to content

Commit f5c4633

Browse files
committed
fix: more cleanup
1 parent 4f4de3a commit f5c4633

5 files changed

Lines changed: 96 additions & 161 deletions

File tree

packages/shared/src/components/modals/post/SmartComposerModal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function SmartComposerModal({
7272
const { audiences, selectedIds, selected, setSelectedIds, userAudienceId } =
7373
useComposerAudience(initialSquadHandle);
7474
const primary = selected[0];
75+
const isMulti = selected.length > 1;
7576

7677
const {
7778
handleSubmit,
@@ -87,8 +88,8 @@ export function SmartComposerModal({
8788
poll,
8889
cover,
8990
primary,
90-
selected,
9191
selectedIds,
92+
isMulti,
9293
initialPreview,
9394
onComplete: () => onRequestClose?.(),
9495
});

packages/shared/src/components/post/composer/LinkForm.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,6 @@ import type { ExternalLinkPreview } from '../../../graphql/posts';
77
import { TITLE_MAX_LENGTH, type LinkFormState } from './types';
88
import { isPreviewForComposerUrl, normalizeComposerUrl } from './utils';
99

10-
const looksLikeRealUrl = (value: string): boolean => {
11-
try {
12-
const url = new URL(value);
13-
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
14-
return false;
15-
}
16-
return url.hostname === 'localhost' || url.hostname.includes('.');
17-
} catch {
18-
return false;
19-
}
20-
};
21-
2210
interface LinkFormProps {
2311
value: LinkFormState;
2412
onChange: (next: LinkFormState) => void;
@@ -41,12 +29,8 @@ export const LinkForm = ({
4129
}, []);
4230

4331
const hasPreviewForUrl = useCallback(
44-
(next?: string): boolean => {
45-
if (!next || !looksLikeRealUrl(next)) {
46-
return false;
47-
}
48-
return !isPreviewForComposerUrl(preview, next);
49-
},
32+
(next?: string): boolean =>
33+
!!next && !isPreviewForComposerUrl(preview, next),
5034
[preview],
5135
);
5236

packages/shared/src/components/post/composer/useComposerSubmit.ts

Lines changed: 79 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import type { FormEvent } from 'react';
2-
import { useCallback, useMemo } from 'react';
2+
import { useCallback } from 'react';
33
import { usePostToSquad } from '../../../hooks';
44
import { useMultipleSourcePost } from '../../../features/squads/hooks/useMultipleSourcePost';
55
import { useToastNotification } from '../../../hooks/useToastNotification';
66
import type {
77
CreatePostInMultipleSourcesArgs,
88
ExternalLinkPreview,
99
} from '../../../graphql/posts';
10-
import type { ApiErrorResult } from '../../../graphql/common';
11-
import { DEFAULT_ERROR } from '../../../graphql/common';
1210
import type { Squad } from '../../../graphql/sources';
1311
import {
1412
POLL_OPTIONS_MIN,
@@ -20,25 +18,22 @@ import {
2018
import type { TextFormCover } from './TextForm';
2119
import { isPreviewForComposerUrl } from './utils';
2220

23-
const isApiErrorResult = (error: unknown): error is ApiErrorResult =>
24-
!!(error as ApiErrorResult)?.response?.errors;
21+
const trimmedOptions = (state: PollFormState): string[] =>
22+
state.options.map((option) => option.trim()).filter(Boolean);
2523

2624
const isTextValid = (state: TextFormState): boolean =>
27-
state.title.trim().length > 0 && state.body.trim().length > 0;
28-
29-
const isLinkValid = (preview?: ExternalLinkPreview): boolean =>
30-
Boolean(preview?.title && (preview.url || preview.permalink));
25+
!!state.title.trim() && !!state.body.trim();
3126

32-
const isPollValid = (state: PollFormState): boolean => {
33-
if (!state.question.trim()) {
34-
return false;
35-
}
36-
const filled = state.options.map((option) => option.trim()).filter(Boolean);
37-
return filled.length >= POLL_OPTIONS_MIN;
38-
};
27+
const isLinkValid = (
28+
state: LinkFormState,
29+
preview: ExternalLinkPreview | undefined,
30+
): boolean =>
31+
!!preview?.title &&
32+
!!(preview.url || preview.permalink) &&
33+
isPreviewForComposerUrl(preview, state.url);
3934

40-
const filledPollOptions = (state: PollFormState): string[] =>
41-
state.options.map((option) => option.trim()).filter(Boolean);
35+
const isPollValid = (state: PollFormState): boolean =>
36+
!!state.question.trim() && trimmedOptions(state).length >= POLL_OPTIONS_MIN;
4237

4338
interface UseComposerSubmitProps {
4439
kind: ComposerKind;
@@ -47,8 +42,8 @@ interface UseComposerSubmitProps {
4742
poll: PollFormState;
4843
cover: TextFormCover | null;
4944
primary: Squad | undefined;
50-
selected: Squad[];
5145
selectedIds: string[];
46+
isMulti: boolean;
5247
initialPreview?: ExternalLinkPreview;
5348
onComplete: () => void;
5449
}
@@ -69,18 +64,12 @@ export const useComposerSubmit = ({
6964
poll,
7065
cover,
7166
primary,
72-
selected,
7367
selectedIds,
68+
isMulti,
7469
initialPreview,
7570
onComplete,
7671
}: UseComposerSubmitProps): UseComposerSubmit => {
7772
const { displayToast } = useToastNotification();
78-
const handleError = useCallback(
79-
(error: ApiErrorResult) => {
80-
displayToast(error.response?.errors?.[0]?.message ?? DEFAULT_ERROR);
81-
},
82-
[displayToast],
83-
);
8473
const {
8574
getLinkPreview,
8675
isLoadingPreview,
@@ -94,176 +83,135 @@ export const useComposerSubmit = ({
9483
onComplete,
9584
displayMutationErrors: true,
9685
});
97-
9886
const { onCreate: createMulti, isPending: isMultiPending } =
99-
useMultipleSourcePost({ onSuccess: onComplete, onError: handleError });
87+
useMultipleSourcePost({
88+
onSuccess: onComplete,
89+
onError: (error) =>
90+
displayToast(error.response?.errors?.[0]?.message ?? 'Failed to post'),
91+
});
10092

10193
const fetchPreview = useCallback(
10294
(url?: string) => {
10395
if (!url) {
10496
return;
10597
}
106-
getLinkPreview(url).catch(() => {
107-
// surfaced via usePostToSquad toast
108-
});
98+
// surfaced via usePostToSquad toast
99+
getLinkPreview(url).catch(() => undefined);
109100
},
110101
[getLinkPreview],
111102
);
112103

113-
const isMulti = selected.length > 1;
114104
const isInFlight = isPosting || isMultiPending;
115105

116-
const isSubmitDisabled = useMemo(() => {
106+
const getIsSubmitDisabled = (): boolean => {
117107
if (isInFlight || !primary) {
118108
return true;
119109
}
120110
if (kind === 'text') {
121111
return !isTextValid(text);
122112
}
123113
if (kind === 'link') {
124-
return (
125-
!isLinkValid(preview) || !isPreviewForComposerUrl(preview, link.url)
126-
);
114+
return !isLinkValid(link, preview);
127115
}
128116
return !isPollValid(poll);
129-
}, [isInFlight, kind, link.url, poll, preview, primary, text]);
117+
};
130118

131-
const submitMulti = useCallback(async () => {
132-
if (kind === 'text') {
119+
const submitText = async () => {
120+
const payload = {
121+
title: text.title.trim(),
122+
content: text.body,
123+
...(cover?.file ? { image: cover.file } : {}),
124+
};
125+
if (isMulti) {
133126
await createMulti({
134127
sourceIds: selectedIds,
135-
title: text.title.trim(),
136-
content: text.body,
137-
...(cover?.file ? { image: cover.file } : {}),
128+
...payload,
138129
} as unknown as CreatePostInMultipleSourcesArgs);
139130
return;
140131
}
141-
if (kind === 'poll') {
132+
await onSubmitFreeformPost(payload, primary as Squad);
133+
};
134+
135+
const submitPoll = async () => {
136+
const options = trimmedOptions(poll);
137+
const duration = poll.durationDays;
138+
if (isMulti) {
142139
await createMulti({
143140
sourceIds: selectedIds,
144141
title: poll.question.trim(),
145-
options: filledPollOptions(poll).map((value, order) => ({
146-
text: value,
147-
order,
148-
})),
149-
...(poll.durationDays != null ? { duration: poll.durationDays } : {}),
142+
options: options.map((value, order) => ({ text: value, order })),
143+
...(duration != null ? { duration } : {}),
150144
} as unknown as CreatePostInMultipleSourcesArgs);
151145
return;
152146
}
147+
await onSubmitPollPost(
148+
{
149+
title: poll.question.trim(),
150+
options,
151+
...(duration != null ? { duration } : {}),
152+
},
153+
primary as Squad,
154+
);
155+
};
156+
157+
const submitLink = async (event: FormEvent<HTMLFormElement>) => {
153158
if (!isPreviewForComposerUrl(preview, link.url)) {
154159
displayToast('Invalid link');
155160
return;
156161
}
157-
158-
const url = preview?.finalUrl ?? preview?.url;
159-
if (!url || !preview?.title) {
162+
const commentary = link.commentary.trim();
163+
if (!isMulti) {
164+
await onSubmitPost(event, primary as Squad, commentary);
160165
return;
161166
}
162-
const commentary = link.commentary.trim();
163-
if (preview.id) {
164-
await createMulti({
165-
sourceIds: selectedIds,
166-
sharedPostId: preview.id,
167-
commentary,
168-
} as unknown as CreatePostInMultipleSourcesArgs);
167+
const url = preview?.finalUrl ?? preview?.url;
168+
if (!url || !preview?.title) {
169169
return;
170170
}
171+
const sharedArgs = preview.id
172+
? { sharedPostId: preview.id }
173+
: { externalLink: url, title: preview.title, imageUrl: preview.image };
171174
await createMulti({
172175
sourceIds: selectedIds,
173-
externalLink: url,
174-
title: preview.title,
175-
imageUrl: preview.image,
176176
commentary,
177+
...sharedArgs,
177178
} as unknown as CreatePostInMultipleSourcesArgs);
178-
}, [
179-
createMulti,
180-
cover?.file,
181-
displayToast,
182-
kind,
183-
link.commentary,
184-
link.url,
185-
poll,
186-
preview,
187-
selectedIds,
188-
text,
189-
]);
179+
};
190180

191-
const submitSingle = useCallback(
181+
const handleSubmit = useCallback(
192182
async (event: FormEvent<HTMLFormElement>) => {
193-
if (!primary) {
183+
event.preventDefault();
184+
if (getIsSubmitDisabled()) {
194185
return;
195186
}
196187
if (kind === 'text') {
197-
await onSubmitFreeformPost(
198-
{
199-
title: text.title.trim(),
200-
content: text.body,
201-
...(cover?.file ? { image: cover.file } : {}),
202-
},
203-
primary,
204-
);
188+
await submitText();
205189
return;
206190
}
207-
if (kind === 'link') {
208-
if (!isPreviewForComposerUrl(preview, link.url)) {
209-
displayToast('Invalid link');
210-
return;
211-
}
212-
213-
await onSubmitPost(event, primary, link.commentary.trim());
191+
if (kind === 'poll') {
192+
await submitPoll();
214193
return;
215194
}
216-
await onSubmitPollPost(
217-
{
218-
title: poll.question.trim(),
219-
options: filledPollOptions(poll),
220-
...(poll.durationDays != null ? { duration: poll.durationDays } : {}),
221-
},
222-
primary,
223-
);
195+
await submitLink(event);
224196
},
197+
// eslint-disable-next-line react-hooks/exhaustive-deps
225198
[
226-
cover?.file,
227-
displayToast,
228199
kind,
229-
link.commentary,
230-
link.url,
231-
onSubmitFreeformPost,
232-
onSubmitPollPost,
233-
onSubmitPost,
234-
poll,
235-
preview,
200+
isMulti,
236201
primary,
202+
preview,
237203
text,
204+
link,
205+
poll,
206+
cover,
207+
selectedIds,
208+
isInFlight,
238209
],
239210
);
240211

241-
const handleSubmit = useCallback(
242-
async (event: FormEvent<HTMLFormElement>) => {
243-
event.preventDefault();
244-
if (isSubmitDisabled) {
245-
return;
246-
}
247-
try {
248-
if (isMulti) {
249-
await submitMulti();
250-
return;
251-
}
252-
await submitSingle(event);
253-
} catch (error) {
254-
if (isApiErrorResult(error)) {
255-
return;
256-
}
257-
258-
throw error;
259-
}
260-
},
261-
[isMulti, isSubmitDisabled, submitMulti, submitSingle],
262-
);
263-
264212
return {
265213
handleSubmit,
266-
isSubmitDisabled,
214+
isSubmitDisabled: getIsSubmitDisabled(),
267215
isInFlight,
268216
preview,
269217
isLoadingPreview,

packages/shared/src/components/post/composer/utils.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { isPreviewForComposerUrl, normalizeComposerUrl } from './utils';
22

33
describe('composer utils', () => {
44
it('normalizes URLs without a protocol', () => {
5-
expect(normalizeComposerUrl('daily.dev')).toBe('https://daily.dev');
5+
expect(normalizeComposerUrl('daily.dev')).toBe('https://daily.dev/');
6+
});
7+
8+
it('rejects values that are not real URLs', () => {
9+
expect(normalizeComposerUrl('aaaaa')).toBe('');
10+
expect(normalizeComposerUrl('https://aaaaa')).toBe('');
611
});
712

813
it('matches previews against the current composer URL', () => {

0 commit comments

Comments
 (0)