diff --git a/packages/pl-fe/src/components/rss-feed-info.tsx b/packages/pl-fe/src/components/rss-feed-info.tsx new file mode 100644 index 000000000..e8ca3d947 --- /dev/null +++ b/packages/pl-fe/src/components/rss-feed-info.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + +import RelativeTimestamp from './relative-timestamp'; +import Avatar from './ui/avatar'; +import HStack from './ui/hstack'; +import Icon from './ui/icon'; +import Stack from './ui/stack'; +import Text from './ui/text'; + +import type { RssFeed } from 'pl-api'; + +interface IRssFeedInfo { + feed: RssFeed; + timestamp: string; + timestampUrl?: string; +} + +const RssFeedInfo: React.FC = ({ feed, timestamp, timestampUrl }) => ( +
+ +
+ +
+ +
+ + + {feed.title} + + + + + + + + + + + + + · + + + {timestampUrl ? ( + event.stopPropagation()} + > + + + ) : ( + + )} + + +
+
+
+); + +export default RssFeedInfo; diff --git a/packages/pl-fe/src/components/status.tsx b/packages/pl-fe/src/components/status.tsx index db0d1c966..bec2cb17a 100644 --- a/packages/pl-fe/src/components/status.tsx +++ b/packages/pl-fe/src/components/status.tsx @@ -30,6 +30,7 @@ import { textForScreenReader } from '@/utils/status'; import EventPreview from './event-preview'; import HashtagLink from './hashtag-link'; import RelativeTimestamp from './relative-timestamp'; +import RssFeedInfo from './rss-feed-info'; import StatusActionBar from './status-action-bar'; import StatusContent from './status-content'; import StatusLanguagePicker from './status-language-picker'; @@ -549,19 +550,23 @@ const Status: React.FC = (props) => { > {statusInfo} - {actualStatus.account_id && ( -
- } - showAccountHoverCard={hoverable} - withLinkToProfile={hoverable} - approvalStatus={actualStatus.approval_status} - avatarSize={avatarSize} - actionAlignment='top' - /> -
+ {status.rss_feed ? ( + + ) : ( + actualStatus.account_id && ( +
+ } + showAccountHoverCard={hoverable} + withLinkToProfile={hoverable} + approvalStatus={actualStatus.approval_status} + avatarSize={avatarSize} + actionAlignment='top' + /> +
+ ) )}
@@ -579,22 +584,26 @@ const Status: React.FC = (props) => { /> )} - + {!status.rss_feed && ( + <> + - {!hideActionBar && ( -
- -
+ {!hideActionBar && ( +
+ +
+ )} + )}
diff --git a/packages/pl-fe/src/features/status/components/detailed-status.tsx b/packages/pl-fe/src/features/status/components/detailed-status.tsx index 7efac0f2f..b6f795c05 100644 --- a/packages/pl-fe/src/features/status/components/detailed-status.tsx +++ b/packages/pl-fe/src/features/status/components/detailed-status.tsx @@ -3,6 +3,7 @@ import React, { useRef } from 'react'; import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl'; import Account from '@/components/account'; +import RssFeedInfo from '@/components/rss-feed-info'; import StatusContent from '@/components/status-content'; import StatusLanguagePicker from '@/components/status-language-picker'; import StatusReactionsBar from '@/components/status-reactions-bar'; @@ -95,7 +96,9 @@ const DetailedStatus: React.FC = ({
{renderStatusInfo()} - {account.id && ( + {actualStatus.rss_feed ? ( + + ) : (
= ({ - + {!status.rss_feed && ( + <> + - - + + - - - - - - - - - {actualStatus.application && ( - <> - + + + + - {actualStatus.application.name} - - - )} - - {actualStatus.edited_at && ( - <> - -
- -
- - )} -
-
-
+ - + {actualStatus.application && ( + <> + + + {actualStatus.application.name} + + + )} - -
-
+ {actualStatus.edited_at && ( + <> + +
+ +
+ + )} +
+ + + + + + +
+
+ + )}
); diff --git a/packages/pl-fe/src/features/status/components/thread.tsx b/packages/pl-fe/src/features/status/components/thread.tsx index 821e317e6..240076d0a 100644 --- a/packages/pl-fe/src/features/status/components/thread.tsx +++ b/packages/pl-fe/src/features/status/components/thread.tsx @@ -356,9 +356,13 @@ const Thread = ({ withMedia={withMedia} /> -
+ {!status.rss_feed && ( + <> +
- + + + )} )} diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index cb4803ada..fc43bee21 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -687,6 +687,15 @@ export const followedTagsRoute = createRoute({ }), }); +export const rssFeedSubscriptionsRoute = createRoute({ + getParentRoute: () => layouts.default, + path: '/rss_feed_subscriptions', + component: RssFeedSubscriptions, + beforeLoad: requireAuthMiddleware(({ context: { features } }) => { + if (!features.rssFeedSubscriptions) throw notFound(); + }), +}); + // Interaction requests export const interactionRequestsRoute = createRoute({ getParentRoute: () => layouts.default, @@ -1460,6 +1469,7 @@ const routeTree = rootRoute.addChildren([ filtersRoute, editFilterRoute, followedTagsRoute, + rssFeedSubscriptionsRoute, interactionRequestsRoute, newStatusRoute, scheduledStatusesRoute, diff --git a/packages/pl-fe/src/features/ui/util/async-components.ts b/packages/pl-fe/src/features/ui/util/async-components.ts index d2a57ac1c..0bfd2e8ae 100644 --- a/packages/pl-fe/src/features/ui/util/async-components.ts +++ b/packages/pl-fe/src/features/ui/util/async-components.ts @@ -96,6 +96,7 @@ export const RegisterInvite = lazy(() => import('@/pages/auth/register-with-invi export const RegistrationPage = lazy(() => import('@/pages/auth/registration')); export const Relays = lazy(() => import('@/pages/dashboard/relays')); export const RemoteTimeline = lazy(() => import('@/pages/timelines/remote-timeline')); +export const RssFeedSubscriptions = lazy(() => import('@/pages/settings/rss-feed-subscriptions')); export const Rules = lazy(() => import('@/pages/dashboard/rules')); export const ScheduledStatuses = lazy(() => import('@/pages/status-lists/scheduled-statuses')); export const Search = lazy(() => import('@/pages/search/search')); diff --git a/packages/pl-fe/src/pages/settings/rss-feed-subscriptions.tsx b/packages/pl-fe/src/pages/settings/rss-feed-subscriptions.tsx new file mode 100644 index 000000000..1312f9f00 --- /dev/null +++ b/packages/pl-fe/src/pages/settings/rss-feed-subscriptions.tsx @@ -0,0 +1,158 @@ +import React from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import List, { ListItem } from '@/components/list'; +import Avatar from '@/components/ui/avatar'; +import Button from '@/components/ui/button'; +import Card, { CardTitle } from '@/components/ui/card'; +import Column from '@/components/ui/column'; +import Form from '@/components/ui/form'; +import HStack from '@/components/ui/hstack'; +import Icon from '@/components/ui/icon'; +import IconButton from '@/components/ui/icon-button'; +import Input from '@/components/ui/input'; +import Stack from '@/components/ui/stack'; +import Text from '@/components/ui/text'; +import { useTextField } from '@/hooks/forms/use-text-field'; +import { + useCreateRssFeedSubscription, + useDeleteRssFeedSubscription, + useRssFeedSubscriptions, +} from '@/queries/rss-feed-subscriptions/use-rss-feed-subscriptions'; +import toast from '@/toast'; + +const messages = defineMessages({ + heading: { id: 'column.rss_feed_subscriptions', defaultMessage: 'Subscribed RSS feeds' }, + label: { id: 'rss_feed_subscriptions.new.title_placeholder', defaultMessage: 'RSS feed URL' }, + createSuccess: { + id: 'rss_feed_subscriptions.add.success', + defaultMessage: 'Successfully subscribed to RSS feed', + }, + createFail: { + id: 'rss_feed_subscriptions.add.fail', + defaultMessage: 'Failed to subsrcibe to RSS feed', + }, +}); + +const NewFeedForm: React.FC = () => { + const intl = useIntl(); + + const url = useTextField(); + + const { mutate: createRssFeedSubscription, isPending } = useCreateRssFeedSubscription(); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + createRssFeedSubscription(url.value, { + onSuccess() { + toast.success(messages.createSuccess); + }, + onError() { + toast.success(messages.createFail); + }, + }); + }; + + const label = intl.formatMessage(messages.label); + + return ( +
+ + + + + +
+ ); +}; + +const RssFeedSubscriptions = () => { + const intl = useIntl(); + + const { data: feeds = [], isLoading } = useRssFeedSubscriptions(); + + const { mutate: deleteRssFeedSubscription, isPending } = useDeleteRssFeedSubscription(); + + const handleDelete = (url: string) => () => { + deleteRssFeedSubscription(url); + }; + + const emptyMessage = ( + + ); + + return ( + + + + } + /> + + + + } + /> + {feeds?.length ? ( + + {feeds?.map((feed) => ( + + {feed.image_url ? ( + + ) : ( + + )} + + {feed.title} + + {feed.url} + + + + + } + /> + ))} + + ) : ( + !isLoading && ( + + {emptyMessage} + + ) + )} + + + ); +}; + +export { RssFeedSubscriptions as default }; diff --git a/packages/pl-fe/src/queries/rss-feed-subscriptions/use-rss-feed-subscriptions.ts b/packages/pl-fe/src/queries/rss-feed-subscriptions/use-rss-feed-subscriptions.ts new file mode 100644 index 000000000..e98fb2e7e --- /dev/null +++ b/packages/pl-fe/src/queries/rss-feed-subscriptions/use-rss-feed-subscriptions.ts @@ -0,0 +1,36 @@ +import { useMutation, useQuery } from '@tanstack/react-query'; + +import { useClient } from '@/hooks/use-client'; + +import { queryClient } from '../client'; + +const useRssFeedSubscriptions = () => { + const client = useClient(); + + return useQuery({ + queryKey: ['rssFeedSubscriptions'], + queryFn: () => client.rssFeedSubscriptions.fetchRssFeedSubscriptions(), + }); +}; + +const useCreateRssFeedSubscription = () => { + const client = useClient(); + + return useMutation({ + mutationKey: ['rss-feed-subscriptions'], + mutationFn: (url: string) => client.rssFeedSubscriptions.createRssFeedSubscription(url), + onSettled: () => queryClient.invalidateQueries({ queryKey: ['rssFeedSubscriptions'] }), + }); +}; + +const useDeleteRssFeedSubscription = () => { + const client = useClient(); + + return useMutation({ + mutationKey: ['rss-feed-subscriptions'], + mutationFn: (url: string) => client.rssFeedSubscriptions.deleteRssFeedSubscription(url), + onSettled: () => queryClient.invalidateQueries({ queryKey: ['rssFeedSubscriptions'] }), + }); +}; + +export { useRssFeedSubscriptions, useCreateRssFeedSubscription, useDeleteRssFeedSubscription };