Merge pull request #2514 from omnivore-app/fix/rss-ui

rss feed reader improvement
This commit is contained in:
Hongbo Wu
2023-07-20 14:59:10 +08:00
committed by GitHub
9 changed files with 152 additions and 66 deletions

View File

@ -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],
}

View File

@ -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',

View File

@ -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

View File

@ -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))
}

View File

@ -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],
},
}
}
}

View File

@ -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],
},
}
}
}

View File

@ -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> = {

View File

@ -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',

View File

@ -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>