diff --git a/packages/api/src/resolvers/subscriptions/index.ts b/packages/api/src/resolvers/subscriptions/index.ts index 3904a5e06..fa556cf3e 100644 --- a/packages/api/src/resolvers/subscriptions/index.ts +++ b/packages/api/src/resolvers/subscriptions/index.ts @@ -257,6 +257,11 @@ export const subscribeResolver = authorized< } } catch (error) { log.error('failed to subscribe', error) + if (error instanceof Error && error.message === 'Status code 404') { + return { + errorCodes: [SubscribeErrorCode.NotFound], + } + } return { errorCodes: [SubscribeErrorCode.BadRequest], } diff --git a/packages/api/src/utils/createTask.ts b/packages/api/src/utils/createTask.ts index 4db367f29..220276ebd 100644 --- a/packages/api/src/utils/createTask.ts +++ b/packages/api/src/utils/createTask.ts @@ -582,6 +582,21 @@ export const enqueueRssFeedFetch = async ( ), } + // If there is no Google Cloud Project Id exposed, it means that we are in local environment + if (env.dev.isLocal || !GOOGLE_CLOUD_PROJECT) { + // Calling the handler function directly. + setTimeout(() => { + axios + .post(env.queue.rssFeedTaskHandlerUrl, payload, { + headers, + }) + .catch((error) => { + console.error(error) + }) + }, 0) + return nanoid() + } + const createdTasks = await createHttpTaskWithToken({ project: GOOGLE_CLOUD_PROJECT, queue: 'omnivore-rss-queue', diff --git a/packages/rss-handler/src/index.ts b/packages/rss-handler/src/index.ts index bbbd537f2..b5f1ef38f 100644 --- a/packages/rss-handler/src/index.ts +++ b/packages/rss-handler/src/index.ts @@ -14,7 +14,7 @@ interface RssFeedRequest { interface ValidRssFeedItem { link: string - isoDate: string + isoDate?: string } function isRssFeedRequest(body: any): body is RssFeedRequest { @@ -166,7 +166,7 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction( for (const item of feed.items) { console.log('Processing feed item', item.link, item.isoDate) - if (!item.link || !item.isoDate) { + if (!item.link) { console.log('Invalid feed item', item) continue } @@ -178,7 +178,7 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction( } // skip old items and items that were published before 24h - const publishedAt = new Date(item.isoDate) + const publishedAt = item.isoDate ? new Date(item.isoDate) : new Date() if ( publishedAt < new Date(lastFetchedAt) || publishedAt < new Date(Date.now() - 24 * 60 * 60 * 1000) @@ -225,7 +225,9 @@ export const rssHandler = Sentry.GCPFunction.wrapHttpFunction( return res.status(500).send('INTERNAL_SERVER_ERROR') } - lastItemFetchedAt = new Date(lastValidItem.isoDate) + lastItemFetchedAt = lastValidItem.isoDate + ? new Date(lastValidItem.isoDate) + : new Date() } // update subscription lastFetchedAt diff --git a/packages/web/lib/dateFormatting.ts b/packages/web/lib/dateFormatting.ts index e31384041..2ae152bad 100644 --- a/packages/web/lib/dateFormatting.ts +++ b/packages/web/lib/dateFormatting.ts @@ -24,3 +24,11 @@ export function formattedShortTime(rawDate: string): string { timeZone, }).format(new Date(rawDate)) } + +export function formattedDateTime(rawDate: string): string { + return new Intl.DateTimeFormat(locale, { + dateStyle: 'short', + timeStyle: 'short', + timeZone, + }).format(new Date(rawDate)) +} diff --git a/packages/web/lib/networking/mutations/subscribeMutation.ts b/packages/web/lib/networking/mutations/subscribeMutation.ts index 1351592a1..807a6049d 100644 --- a/packages/web/lib/networking/mutations/subscribeMutation.ts +++ b/packages/web/lib/networking/mutations/subscribeMutation.ts @@ -9,9 +9,16 @@ type SubscribeResult = { subscribe: Subscribe } +enum SubscribeErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED', + AlreadySubscribed = 'ALREADY_SUBSCRIBED', +} + type Subscribe = { - subscriptions: Subscription[] - errorCodes?: unknown[] + subscriptions?: Subscription[] + errorCodes?: SubscribeErrorCode[] } export type SubscribeMutationInput = { @@ -22,7 +29,7 @@ export type SubscribeMutationInput = { export async function subscribeMutation( input: SubscribeMutationInput -): Promise { +): Promise { const mutation = gql` mutation Subscribe($input: SubscribeInput!) { subscribe(input: $input) { @@ -39,9 +46,13 @@ export async function subscribeMutation( ` try { const data = (await gqlFetcher(mutation, { input })) as SubscribeResult - return data.subscribe.errorCodes ? undefined : data.subscribe + return data } catch (error) { console.log('subscribeMutation error', error) - return undefined + return { + subscribe: { + errorCodes: [SubscribeErrorCode.BadRequest], + }, + } } } diff --git a/packages/web/lib/networking/mutations/updateSubscriptionMutation.ts b/packages/web/lib/networking/mutations/updateSubscriptionMutation.ts index a3f4e20e6..b623ff482 100644 --- a/packages/web/lib/networking/mutations/updateSubscriptionMutation.ts +++ b/packages/web/lib/networking/mutations/updateSubscriptionMutation.ts @@ -6,9 +6,15 @@ interface UpdateSubscriptionResult { updateSubscription: UpdateSubscription } +export enum UpdateSubscriptionErrorCode { + BadRequest = 'BAD_REQUEST', + NotFound = 'NOT_FOUND', + Unauthorized = 'UNAUTHORIZED', +} + interface UpdateSubscription { - subscription: Subscription - errorCodes?: unknown[] + subscription?: Subscription + errorCodes?: UpdateSubscriptionErrorCode[] } interface UpdateSubscriptionInput { @@ -20,7 +26,7 @@ interface UpdateSubscriptionInput { export async function updateSubscriptionMutation( input: UpdateSubscriptionInput -): Promise { +): Promise { const mutation = gql` mutation UpdateSubscription($input: UpdateSubscriptionInput!) { updateSubscription(input: $input) { @@ -41,11 +47,13 @@ export async function updateSubscriptionMutation( const data = (await gqlFetcher(mutation, { input, })) as UpdateSubscriptionResult - return data.updateSubscription.errorCodes - ? undefined - : data.updateSubscription.subscription.id + return data } catch (error) { console.log('updateSubscriptionMutation error', error) - return undefined + return { + updateSubscription: { + errorCodes: [UpdateSubscriptionErrorCode.BadRequest], + }, + } } } diff --git a/packages/web/locales/en/messages.ts b/packages/web/locales/en/messages.ts index 306e427c1..9b505ffe4 100644 --- a/packages/web/locales/en/messages.ts +++ b/packages/web/locales/en/messages.ts @@ -25,7 +25,9 @@ const errorMessages: Record = { "Your sign up page has timed out, you'll be redirected to Google sign in page to authenticate again.", 'error.USER_EXISTS': 'User with this email exists already', 'error.UNKNOWN': 'An unknown error occurred', - 'error.INVALID_PASSWORD': 'Invalid password. Password must be at least 8 chars.' + 'error.INVALID_PASSWORD': 'Invalid password. Password must be at least 8 chars.', + 'error.ALREADY_SUBSCRIBED': 'You are already subscribed to this feed', + 'error.BAD_REQUEST': 'Bad request', } const loginPageMessages: Record = { diff --git a/packages/web/pages/settings/rss/add.tsx b/packages/web/pages/settings/rss/add.tsx index da7196792..7d68fd181 100644 --- a/packages/web/pages/settings/rss/add.tsx +++ b/packages/web/pages/settings/rss/add.tsx @@ -14,6 +14,7 @@ import { SettingsLayout } from '../../../components/templates/SettingsLayout' import { subscribeMutation } from '../../../lib/networking/mutations/subscribeMutation' import { SubscriptionType } from '../../../lib/networking/queries/useGetSubscriptionsQuery' import { showSuccessToast } from '../../../lib/toastHelpers' +import { formatMessage } from '../../../locales/en/messages' // Styles const Header = styled(Box, { @@ -30,20 +31,35 @@ export default function AddRssFeed(): JSX.Element { const [feedUrl, setFeedUrl] = useState('') const subscribe = useCallback(async () => { - try { - const result = await subscribeMutation({ - url: feedUrl, - subscriptionType: SubscriptionType.RSS, - }) - if (result) { - router.push(`/settings/rss`) - showSuccessToast('New RSS feed has been added.') - } else { - setErrorMessage('There was an error adding new RSS feed.') - } - } catch (err) { - setErrorMessage('Error: ' + err) + if (!feedUrl) { + setErrorMessage('Please enter a valid RSS feed URL') + return } + + let normailizedUrl: string + // normalize the url + try { + normailizedUrl = new URL(feedUrl).toString() + } catch (e) { + setErrorMessage('Please enter a valid RSS feed URL') + return + } + + const result = await subscribeMutation({ + url: normailizedUrl, + subscriptionType: SubscriptionType.RSS, + }) + + if (result.subscribe.errorCodes) { + const errorMessage = formatMessage({ + id: `error.${result.subscribe.errorCodes[0]}`, + }) + setErrorMessage(`There was an error adding new RSS feed: ${errorMessage}`) + return + } + + router.push(`/settings/rss`) + showSuccessToast('New RSS feed has been added.') }, [feedUrl, router]) return ( @@ -78,12 +94,9 @@ export default function AddRssFeed(): JSX.Element { value={feedUrl} placeholder={'Enter the RSS feed URL here'} onChange={(e) => { - e.preventDefault() + setErrorMessage(undefined) setFeedUrl(e.target.value) }} - disabled={false} - hidden={false} - required={true} css={{ border: '1px solid $textNonessential', borderRadius: '8px', diff --git a/packages/web/pages/settings/rss/index.tsx b/packages/web/pages/settings/rss/index.tsx index 17ddb78d9..0312615b4 100644 --- a/packages/web/pages/settings/rss/index.tsx +++ b/packages/web/pages/settings/rss/index.tsx @@ -11,7 +11,7 @@ import { SettingsTableRow, } from '../../../components/templates/settings/SettingsTable' import { theme } from '../../../components/tokens/stitches.config' -import { formattedShortTime } from '../../../lib/dateFormatting' +import { formattedDateTime } from '../../../lib/dateFormatting' import { unsubscribeMutation } from '../../../lib/networking/mutations/unsubscribeMutation' import { updateSubscriptionMutation } from '../../../lib/networking/mutations/updateSubscriptionMutation' import { @@ -20,6 +20,7 @@ import { } from '../../../lib/networking/queries/useGetSubscriptionsQuery' import { applyStoredTheme } from '../../../lib/themeUpdater' import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers' +import { formatMessage } from '../../../locales/en/messages' export default function Rss(): JSX.Element { const router = useRouter() @@ -28,19 +29,25 @@ export default function Rss(): JSX.Element { ) const [onDeleteId, setOnDeleteId] = useState('') const [onEditId, setOnEditId] = useState('') - const [name, setName] = useState('') + const [onEditName, setOnEditName] = useState('') async function updateSubscription(): Promise { const result = await updateSubscriptionMutation({ id: onEditId, - name, + name: onEditName, }) - if (result) { - showSuccessToast('RSS feed updated', { position: 'bottom-right' }) - } else { - showErrorToast('Failed to update', { position: 'bottom-right' }) + + if (result.updateSubscription.errorCodes) { + const errorMessage = formatMessage({ + id: `error.${result.updateSubscription.errorCodes[0]}`, + }) + showErrorToast(`failed to update subscription: ${errorMessage}`, { + position: 'bottom-right', + }) + return } + showSuccessToast('RSS feed updated', { position: 'bottom-right' }) revalidate() } @@ -76,20 +83,24 @@ export default function Rss(): JSX.Element { - e.stopPropagation()} - onChange={(e) => setName(e.target.value)} - placeholder="Description" - disabled={!onEditId} - /> - {onEditId ? ( - + onEditId === subscription.id ? ( + + e.stopPropagation()} + onChange={(e) => setOnEditName(e.target.value)} + placeholder="Description" + css={{ + m: '0px', + fontSize: '18px', + '@mdDown': { + fontSize: '12px', + fontWeight: 'bold', + }, + width: '400px', + }} + /> + - Save - + /> { e.stopPropagation() setOnEditId('') + setOnEditName('') }} - > - Cancel - + /> - ) : ( + + ) : ( + + + {subscription.name} + { e.stopPropagation() - setName(subscription.name) + setOnEditName(subscription.name) setOnEditId(subscription.id) }} /> - )} - + + ) } isLast={i === subscriptions.length - 1} onDelete={() => { @@ -141,7 +163,7 @@ export default function Rss(): JSX.Element { {`URL: ${subscription.url}, `} {`Last fetched: ${ subscription.lastFetchedAt - ? formattedShortTime(subscription.lastFetchedAt) + ? formattedDateTime(subscription.lastFetchedAt) : 'Never' }`}