nicolium: downstream ports
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
77
packages/pl-fe/src/components/rss-feed-info.tsx
Normal file
77
packages/pl-fe/src/components/rss-feed-info.tsx
Normal file
@ -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<IRssFeedInfo> = ({ feed, timestamp, timestampUrl }) => (
|
||||
<div className='group block w-full shrink-0'>
|
||||
<HStack alignItems='center' space={3} className='overflow-hidden'>
|
||||
<div className='rounded-lg'>
|
||||
<Avatar src={feed.image_url || ''} size={42} alt={feed.title || ''} />
|
||||
</div>
|
||||
|
||||
<div className='grow overflow-hidden'>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text size='sm' weight='semibold' truncate>
|
||||
{feed.title}
|
||||
</Text>
|
||||
</HStack>
|
||||
|
||||
<Stack>
|
||||
<HStack alignItems='center' space={1}>
|
||||
<Text theme='muted' size='sm'>
|
||||
<FormattedMessage id='rss_feed.label' defaultMessage='RSS Feed' />
|
||||
</Text>
|
||||
|
||||
<Icon
|
||||
className='size-4 text-gray-700 dark:text-gray-600'
|
||||
src={require('@phosphor-icons/core/regular/rss.svg')}
|
||||
/>
|
||||
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
·
|
||||
</Text>
|
||||
|
||||
{timestampUrl ? (
|
||||
<Link
|
||||
to={timestampUrl}
|
||||
className='hover:underline'
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<RelativeTimestamp
|
||||
timestamp={timestamp}
|
||||
theme='muted'
|
||||
size='sm'
|
||||
className='whitespace-nowrap'
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<RelativeTimestamp
|
||||
timestamp={timestamp}
|
||||
theme='muted'
|
||||
size='sm'
|
||||
className='whitespace-nowrap'
|
||||
/>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default RssFeedInfo;
|
||||
@ -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<IStatus> = (props) => {
|
||||
>
|
||||
{statusInfo}
|
||||
|
||||
{actualStatus.account_id && (
|
||||
<div className='flex'>
|
||||
<AccountContainer
|
||||
key={actualStatus.account_id}
|
||||
id={actualStatus.account_id}
|
||||
action={<AccountInfo status={actualStatus} />}
|
||||
showAccountHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
approvalStatus={actualStatus.approval_status}
|
||||
avatarSize={avatarSize}
|
||||
actionAlignment='top'
|
||||
/>
|
||||
</div>
|
||||
{status.rss_feed ? (
|
||||
<RssFeedInfo feed={status.rss_feed} timestamp={status.created_at} />
|
||||
) : (
|
||||
actualStatus.account_id && (
|
||||
<div className='flex'>
|
||||
<AccountContainer
|
||||
key={actualStatus.account_id}
|
||||
id={actualStatus.account_id}
|
||||
action={<AccountInfo status={actualStatus} />}
|
||||
showAccountHoverCard={hoverable}
|
||||
withLinkToProfile={hoverable}
|
||||
approvalStatus={actualStatus.approval_status}
|
||||
avatarSize={avatarSize}
|
||||
actionAlignment='top'
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className='status__content-wrapper'>
|
||||
@ -579,22 +584,26 @@ const Status: React.FC<IStatus> = (props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
<StatusReactionsBar status={actualStatus} collapsed />
|
||||
{!status.rss_feed && (
|
||||
<>
|
||||
<StatusReactionsBar status={actualStatus} collapsed />
|
||||
|
||||
{!hideActionBar && (
|
||||
<div
|
||||
className={clsx({
|
||||
'pt-2': actualStatus.emoji_reactions.length,
|
||||
'pt-4': !actualStatus.emoji_reactions.length,
|
||||
})}
|
||||
>
|
||||
<StatusActionBar
|
||||
status={actualStatus}
|
||||
rebloggedBy={isReblog ? status.account : undefined}
|
||||
fromBookmarks={fromBookmarks}
|
||||
expandable
|
||||
/>
|
||||
</div>
|
||||
{!hideActionBar && (
|
||||
<div
|
||||
className={clsx({
|
||||
'pt-2': actualStatus.emoji_reactions.length,
|
||||
'pt-4': !actualStatus.emoji_reactions.length,
|
||||
})}
|
||||
>
|
||||
<StatusActionBar
|
||||
status={actualStatus}
|
||||
rebloggedBy={isReblog ? status.account : undefined}
|
||||
fromBookmarks={fromBookmarks}
|
||||
expandable
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@ -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<IDetailedStatus> = ({
|
||||
<div ref={node} className='detailed-actualStatus' tabIndex={-1}>
|
||||
{renderStatusInfo()}
|
||||
|
||||
{account.id && (
|
||||
{actualStatus.rss_feed ? (
|
||||
<RssFeedInfo feed={actualStatus.rss_feed} timestamp={actualStatus.created_at} />
|
||||
) : (
|
||||
<div className='mb-4'>
|
||||
<Account
|
||||
key={account.id}
|
||||
@ -115,83 +118,87 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
||||
<StatusReactionsBar status={actualStatus} />
|
||||
{!status.rss_feed && (
|
||||
<>
|
||||
<StatusReactionsBar status={actualStatus} />
|
||||
|
||||
<HStack space={2} justifyContent='between' alignItems='center' className='py-3' wrap>
|
||||
<StatusInteractionBar status={actualStatus} />
|
||||
<HStack space={2} justifyContent='between' alignItems='center' className='py-3' wrap>
|
||||
<StatusInteractionBar status={actualStatus} />
|
||||
|
||||
<HStack space={1} alignItems='center'>
|
||||
<span>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<HStack space={1} alignItems='center' wrap>
|
||||
<a
|
||||
href={actualStatus.url}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='hover:underline'
|
||||
>
|
||||
<FormattedDate
|
||||
value={new Date(actualStatus.created_at)}
|
||||
hour12
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
hour='numeric'
|
||||
minute='2-digit'
|
||||
/>
|
||||
</a>
|
||||
|
||||
{actualStatus.application && (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
<HStack space={1} alignItems='center'>
|
||||
<span>
|
||||
<Text tag='span' theme='muted' size='sm'>
|
||||
<HStack space={1} alignItems='center' wrap>
|
||||
<a
|
||||
href={actualStatus.application.website ?? '#'}
|
||||
href={actualStatus.url}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='hover:underline'
|
||||
title={intl.formatMessage(messages.applicationName, {
|
||||
name: actualStatus.application.name,
|
||||
})}
|
||||
>
|
||||
{actualStatus.application.name}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='status.edited'
|
||||
defaultMessage='Edited {date}'
|
||||
values={{
|
||||
date: intl.formatDate(new Date(actualStatus.edited_at), {
|
||||
hour12: true,
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
}}
|
||||
<FormattedDate
|
||||
value={new Date(actualStatus.created_at)}
|
||||
hour12
|
||||
year='numeric'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
hour='numeric'
|
||||
minute='2-digit'
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</Text>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<StatusTypeIcon visibility={actualStatus.visibility} />
|
||||
{actualStatus.application && (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
<a
|
||||
href={actualStatus.application.website ?? '#'}
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className='hover:underline'
|
||||
title={intl.formatMessage(messages.applicationName, {
|
||||
name: actualStatus.application.name,
|
||||
})}
|
||||
>
|
||||
{actualStatus.application.name}
|
||||
</a>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StatusLanguagePicker status={actualStatus} showLabel />
|
||||
</HStack>
|
||||
</HStack>
|
||||
{actualStatus.edited_at && (
|
||||
<>
|
||||
<span className='⁂-separator' />
|
||||
<div
|
||||
className='inline hover:underline'
|
||||
onClick={handleOpenCompareHistoryModal}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='status.edited'
|
||||
defaultMessage='Edited {date}'
|
||||
values={{
|
||||
date: intl.formatDate(new Date(actualStatus.edited_at), {
|
||||
hour12: true,
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</HStack>
|
||||
</Text>
|
||||
</span>
|
||||
|
||||
<StatusTypeIcon visibility={actualStatus.visibility} />
|
||||
|
||||
<StatusLanguagePicker status={actualStatus} showLabel />
|
||||
</HStack>
|
||||
</HStack>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -356,9 +356,13 @@ const Thread = ({
|
||||
withMedia={withMedia}
|
||||
/>
|
||||
|
||||
<hr className='-mx-4 mb-2 max-w-[100vw] border-t-2 black:border-t dark:border-gray-800' />
|
||||
{!status.rss_feed && (
|
||||
<>
|
||||
<hr className='-mx-4 mb-2 max-w-[100vw] border-t-2 black:border-t dark:border-gray-800' />
|
||||
|
||||
<StatusActionBar status={status} expandable={isModal} space='lg' withLabels />
|
||||
<StatusActionBar status={status} expandable={isModal} space='lg' withLabels />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Hotkeys>
|
||||
)}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'));
|
||||
|
||||
158
packages/pl-fe/src/pages/settings/rss-feed-subscriptions.tsx
Normal file
158
packages/pl-fe/src/pages/settings/rss-feed-subscriptions.tsx
Normal file
@ -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<Element>) => {
|
||||
e.preventDefault();
|
||||
createRssFeedSubscription(url.value, {
|
||||
onSuccess() {
|
||||
toast.success(messages.createSuccess);
|
||||
},
|
||||
onError() {
|
||||
toast.success(messages.createFail);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<label className='grow'>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<Input type='text' placeholder={label} disabled={isPending} {...url} />
|
||||
</label>
|
||||
|
||||
<Button disabled={isPending} onClick={handleSubmit} theme='primary'>
|
||||
<FormattedMessage
|
||||
id='rss_feed_subscriptions.new.create_title'
|
||||
defaultMessage='Subscribe'
|
||||
/>
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
const RssFeedSubscriptions = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data: feeds = [], isLoading } = useRssFeedSubscriptions();
|
||||
|
||||
const { mutate: deleteRssFeedSubscription, isPending } = useDeleteRssFeedSubscription();
|
||||
|
||||
const handleDelete = (url: string) => () => {
|
||||
deleteRssFeedSubscription(url);
|
||||
};
|
||||
|
||||
const emptyMessage = (
|
||||
<FormattedMessage
|
||||
id='empty_column.rss_feed_subscriptions'
|
||||
defaultMessage="You haven't subscribed to any RSS feeds yet."
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Stack space={4}>
|
||||
<CardTitle
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='rss_feed_subscriptions.new.heading'
|
||||
defaultMessage='Subscribe to a new RSS feed'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<NewFeedForm />
|
||||
|
||||
<CardTitle
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='rss_feed_subscriptions.list.heading'
|
||||
defaultMessage='Subscribed feeds'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{feeds?.length ? (
|
||||
<List>
|
||||
{feeds?.map((feed) => (
|
||||
<ListItem
|
||||
key={feed.id}
|
||||
label={
|
||||
<HStack className='w-full' alignItems='center' space={2}>
|
||||
{feed.image_url ? (
|
||||
<Avatar size={40} src={feed.image_url} />
|
||||
) : (
|
||||
<Icon src={require('@phosphor-icons/core/regular/rss.svg')} size={40} />
|
||||
)}
|
||||
<Stack className='flex-1'>
|
||||
<span>{feed.title}</span>
|
||||
<Text size='sm' theme='muted' truncate>
|
||||
{feed.url}
|
||||
</Text>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={handleDelete(feed.url)}
|
||||
disabled={isPending}
|
||||
className='size-8 text-gray-700 dark:text-gray-600'
|
||||
src={require('@phosphor-icons/core/regular/x.svg')}
|
||||
/>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
!isLoading && (
|
||||
<Card variant='rounded' size='lg'>
|
||||
{emptyMessage}
|
||||
</Card>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { RssFeedSubscriptions as default };
|
||||
@ -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 };
|
||||
Reference in New Issue
Block a user