Merge pull request #2514 from omnivore-app/fix/rss-ui
rss feed reader improvement
This commit is contained in:
@ -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],
|
||||
}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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<any | undefined> {
|
||||
): Promise<SubscribeResult> {
|
||||
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],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<any | undefined> {
|
||||
): Promise<UpdateSubscriptionResult> {
|
||||
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],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,7 +25,9 @@ const errorMessages: Record<string, string> = {
|
||||
"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<string, string> = {
|
||||
|
||||
@ -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<string>('')
|
||||
|
||||
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',
|
||||
|
||||
@ -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<string>('')
|
||||
const [onEditId, setOnEditId] = useState('')
|
||||
const [name, setName] = useState('')
|
||||
const [onEditName, setOnEditName] = useState('')
|
||||
|
||||
async function updateSubscription(): Promise<void> {
|
||||
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 {
|
||||
<SettingsTableRow
|
||||
key={subscription.id}
|
||||
title={
|
||||
<HStack
|
||||
alignment={'center'}
|
||||
distribution={'start'}
|
||||
css={{ width: '400px' }}
|
||||
>
|
||||
<FormInput
|
||||
value={onEditId ? name : subscription.name}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Description"
|
||||
disabled={!onEditId}
|
||||
/>
|
||||
{onEditId ? (
|
||||
<HStack alignment={'center'} distribution={'start'}>
|
||||
onEditId === subscription.id ? (
|
||||
<HStack alignment={'center'} distribution={'start'}>
|
||||
<FormInput
|
||||
value={onEditName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setOnEditName(e.target.value)}
|
||||
placeholder="Description"
|
||||
css={{
|
||||
m: '0px',
|
||||
fontSize: '18px',
|
||||
'@mdDown': {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
width: '400px',
|
||||
}}
|
||||
/>
|
||||
<HStack>
|
||||
<FloppyDisk
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
color={theme.colors.omnivoreCtaYellow.toString()}
|
||||
@ -98,32 +109,43 @@ export default function Rss(): JSX.Element {
|
||||
await updateSubscription()
|
||||
setOnEditId('')
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</FloppyDisk>
|
||||
/>
|
||||
<XCircle
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
color={theme.colors.omnivoreRed.toString()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setOnEditId('')
|
||||
setOnEditName('')
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</XCircle>
|
||||
/>
|
||||
</HStack>
|
||||
) : (
|
||||
</HStack>
|
||||
) : (
|
||||
<HStack alignment={'center'} distribution={'start'}>
|
||||
<StyledText
|
||||
css={{
|
||||
m: '0px',
|
||||
fontSize: '18px',
|
||||
'@mdDown': {
|
||||
fontSize: '12px',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{subscription.name}
|
||||
</StyledText>
|
||||
<Pencil
|
||||
style={{ cursor: 'pointer', marginLeft: '5px' }}
|
||||
color={theme.colors.omnivoreLightGray.toString()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setName(subscription.name)
|
||||
setOnEditName(subscription.name)
|
||||
setOnEditId(subscription.id)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</HStack>
|
||||
)
|
||||
}
|
||||
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'
|
||||
}`}
|
||||
</StyledText>
|
||||
|
||||
Reference in New Issue
Block a user