Files
omnivore/packages/web/pages/settings/feeds/index.tsx
2024-08-27 09:22:34 +08:00

345 lines
12 KiB
TypeScript

import { FloppyDisk, Pencil, XCircle } from '@phosphor-icons/react'
import { useRouter } from 'next/router'
import { useMemo, useState } from 'react'
import { FormInput } from '../../../components/elements/FormElements'
import {
HStack,
SpanBox,
VStack,
} from '../../../components/elements/LayoutPrimitives'
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
import {
EmptySettingsRow,
SettingsTable,
SettingsTableRow,
} from '../../../components/templates/settings/SettingsTable'
import { theme } from '../../../components/tokens/stitches.config'
import { formattedDateTime } from '../../../lib/dateFormatting'
import { unsubscribeMutation } from '../../../lib/networking/mutations/unsubscribeMutation'
import {
UpdateSubscriptionInput,
updateSubscriptionMutation,
} from '../../../lib/networking/mutations/updateSubscriptionMutation'
import {
FetchContentType,
Subscription,
SubscriptionStatus,
SubscriptionType,
useGetSubscriptionsQuery,
} 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()
const subscriptionsResponse = useGetSubscriptionsQuery(
SubscriptionType.RSS
)
const subscriptions = subscriptionsResponse.subscriptions as Array<
Subscription & { type: SubscriptionType.RSS }
>
const { isValidating, revalidate } = subscriptionsResponse
const [onDeleteId, setOnDeleteId] = useState<string>('')
const [onEditId, setOnEditId] = useState('')
const [onEditName, setOnEditName] = useState('')
const [onPauseId, setOnPauseId] = useState('')
const [onEditStatus, setOnEditStatus] = useState<SubscriptionStatus>()
const sortedSubscriptions = useMemo(() => {
if (!subscriptions) {
return []
}
return subscriptions
.filter((s) => s.status == 'ACTIVE')
.sort((a, b) => a.name.localeCompare(b.name))
}, [subscriptions])
async function updateSubscription(
input: UpdateSubscriptionInput
): Promise<void> {
const result = await updateSubscriptionMutation(input)
if (result.updateSubscription.errorCodes) {
const errorMessage = formatMessage({
id: `error.${result.updateSubscription.errorCodes[0]}`,
})
showErrorToast(`failed to update subscription: ${errorMessage}`, {
position: 'bottom-right',
})
return
}
showSuccessToast('Feed updated', { position: 'bottom-right' })
revalidate()
}
async function onDelete(id: string): Promise<void> {
const result = await unsubscribeMutation('', id)
if (result) {
showSuccessToast('Feed unsubscribed', { position: 'bottom-right' })
} else {
showErrorToast('Failed to unsubscribe', { position: 'bottom-right' })
}
revalidate()
}
async function onPause(
id: string,
status: SubscriptionStatus = 'UNSUBSCRIBED'
): Promise<void> {
const result = await updateSubscriptionMutation({
id,
status,
})
const action = status == 'UNSUBSCRIBED' ? 'pause' : 'resume'
if (result) {
showSuccessToast(`Feed ${action}d`, {
position: 'bottom-right',
})
} else {
showErrorToast(`Failed to ${action}`, { position: 'bottom-right' })
}
revalidate()
}
const updateFetchContent = async (
id: string,
fetchContent: FetchContentType
): Promise<void> => {
const result = await updateSubscriptionMutation({
id,
fetchContentType: fetchContent,
})
if (result) {
showSuccessToast(`Updated feed fetch rule`)
} else {
showErrorToast(`Error updating feed fetch rule`)
}
revalidate()
}
applyStoredTheme()
return (
<SettingsTable
pageId={'feeds'}
pageInfoLink="https://docs.omnivore.app/using/feeds.html"
headerTitle="Subscribed feeds"
createTitle="Add a feed"
createAction={() => {
router.push('/settings/feeds/add')
}}
>
{sortedSubscriptions.length === 0 ? (
<EmptySettingsRow text={isValidating ? '-' : 'No feeds subscribed'} />
) : (
sortedSubscriptions.map((subscription, i) => {
return (
<SettingsTableRow
key={subscription.id}
title={
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()}
onClick={async (e) => {
e.stopPropagation()
await updateSubscription({
id: onEditId,
name: onEditName,
})
setOnEditId('')
}}
/>
<XCircle
style={{ cursor: 'pointer', marginLeft: '5px' }}
color={theme.colors.omnivoreRed.toString()}
onClick={(e) => {
e.stopPropagation()
setOnEditId('')
setOnEditName('')
}}
/>
</HStack>
</HStack>
) : (
<HStack alignment={'center'} distribution={'start'}>
<SpanBox
css={{
m: '0px',
fontSize: '18px',
'@mdDown': {
fontSize: '12px',
fontWeight: 'bold',
},
}}
>
{subscription.name}
</SpanBox>
<Pencil
style={{ cursor: 'pointer', marginLeft: '5px' }}
color={theme.colors.omnivoreLightGray.toString()}
onClick={(e) => {
e.stopPropagation()
setOnEditName(subscription.name)
setOnEditId(subscription.id)
}}
/>
</HStack>
)
}
isLast={i === sortedSubscriptions.length - 1}
onDelete={() => {
console.log('onDelete triggered: ', subscription.id)
setOnDeleteId(subscription.id)
}}
deleteTitle="Unsubscribe"
sublineElement={
<VStack
css={{
my: '8px',
fontSize: '11px',
}}
>
<SpanBox>{`URL: ${subscription.url}`}</SpanBox>
{/* show failed timestamp instead of last refreshed timestamp if the feed failed to refresh */}
{subscription.failedAt ? (
<SpanBox
css={{ color: 'red' }}
>{`Failed to refresh: ${formattedDateTime(
subscription.failedAt
)}`}</SpanBox>
) : (
<SpanBox>{`Last refreshed: ${
subscription.lastFetchedAt
? formattedDateTime(subscription.lastFetchedAt)
: 'Never'
}`}</SpanBox>
)}
<SpanBox>
{subscription.mostRecentItemDate &&
`Most recent item: ${formattedDateTime(
subscription.mostRecentItemDate
)}`}
</SpanBox>
<select
tabIndex={-1}
onChange={(event) => {
;(async () => {
updateFetchContent(
subscription.id,
event.target.value as FetchContentType
)
})()
}}
defaultValue={subscription.fetchContentType}
style={{
padding: '5px',
marginTop: '5px',
borderRadius: '6px',
minWidth: '196px',
}}
onClick={(event) => {
event.stopPropagation()
}}
>
<option value="ALWAYS">Fetch link: Always</option>
<option value="NEVER">Fetch link: Never</option>
<option value="WHEN_EMPTY">Fetch link: When empty</option>
</select>
</VStack>
}
onClick={() => {
router.push(
`/search?q=in:inbox rss:"${encodeURIComponent(
subscription.url
)}"`
)
}}
// extraElement={
// <HStack
// distribution="start"
// alignment="center"
// css={{
// padding: '0 5px',
// }}
// >
// <CheckboxComponent
// checked={!!subscription.autoAddToLibrary}
// setChecked={async (checked) => {
// await updateSubscriptionMutation({
// id: subscription.id,
// autoAddToLibrary: checked,
// })
// revalidate()
// }}
// />
// <SpanBox
// css={{
// padding: '0 5px',
// fontSize: '12px',
// }}
// >
// Auto add to library
// </SpanBox>
// </HStack>
// }
/>
)
})
)}
{onDeleteId && (
<ConfirmationModal
message={'Feed will be unsubscribed. This action cannot be undone.'}
onAccept={async () => {
await onDelete(onDeleteId)
setOnDeleteId('')
}}
onOpenChange={() => setOnDeleteId('')}
/>
)}
{onPauseId && (
<ConfirmationModal
message={`Feed will be ${
onEditStatus === 'UNSUBSCRIBED' ? 'paused' : 'resumed'
}. You can ${
onEditStatus === 'UNSUBSCRIBED' ? 'resume' : 'pause'
} it at any time.`}
onAccept={async () => {
await onPause(onPauseId, onEditStatus)
setOnPauseId('')
setOnEditStatus(undefined)
}}
onOpenChange={() => {
setOnPauseId('')
setOnEditStatus(undefined)
}}
/>
)}
</SettingsTable>
)
}