pl-fe: move files around
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useAccountLookup } from 'pl-fe/api/hooks/accounts/use-account-lookup';
|
||||
import LoadMore from 'pl-fe/components/load-more';
|
||||
import MissingIndicator from 'pl-fe/components/missing-indicator';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import { type AccountGalleryAttachment, useAccountGallery } from 'pl-fe/hooks/use-account-gallery';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
|
||||
import MediaItem from './components/media-item';
|
||||
|
||||
const AccountGallery = () => {
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const { openModal } = useModalsStore();
|
||||
|
||||
const {
|
||||
account,
|
||||
isLoading: accountLoading,
|
||||
isUnavailable,
|
||||
} = useAccountLookup(username, { withRelationship: true });
|
||||
|
||||
const { data: attachments, isFetching, isLoading, hasNextPage: hasMore, fetchNextPage } = useAccountGallery(account?.id!);
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
if (hasMore) {
|
||||
handleLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
fetchNextPage({ cancelRefetch: false });
|
||||
};
|
||||
|
||||
const handleLoadOlder: React.MouseEventHandler = e => {
|
||||
e.preventDefault();
|
||||
handleScrollToBottom();
|
||||
};
|
||||
|
||||
const handleOpenMedia = (attachment: AccountGalleryAttachment) => {
|
||||
if (attachment.type === 'video') {
|
||||
openModal('VIDEO', { media: attachment, statusId: attachment.status_id });
|
||||
} else {
|
||||
openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id });
|
||||
}
|
||||
};
|
||||
|
||||
if (accountLoading || isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isFetching && attachments.length === 0)) {
|
||||
loadOlder = <LoadMore className='my-auto mt-4' visible={!isFetching} onClick={handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (isUnavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={`@${account.acct}`} transparent withHeader={false}>
|
||||
<div role='feed' className='grid grid-cols-2 gap-1 overflow-hidden rounded-md sm:grid-cols-3'>
|
||||
{attachments.map((attachment, index) => (
|
||||
<MediaItem
|
||||
key={`${attachment.status_id}+${attachment.id}`}
|
||||
attachment={attachment}
|
||||
onOpenMedia={handleOpenMedia}
|
||||
isLast={index === attachments.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!isLoading && attachments.length === 0 && (
|
||||
<div className='empty-column-indicator col-span-2 sm:col-span-3'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loadOlder}
|
||||
|
||||
{isFetching && attachments.length === 0 && (
|
||||
<div className='relative flex-auto px-8 py-4'>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountGallery as default };
|
||||
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'pl-fe/components/account';
|
||||
import Icon from 'pl-fe/components/icon';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
|
||||
import type { Account as AccountEntity } from 'pl-fe/normalizers/account';
|
||||
|
||||
interface IMovedNote {
|
||||
from: AccountEntity;
|
||||
to: AccountEntity;
|
||||
}
|
||||
|
||||
const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
||||
<div className='p-4'>
|
||||
<HStack className='mb-2' alignItems='center' space={1.5}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/briefcase.svg')}
|
||||
className='flex-none text-primary-600 dark:text-primary-400'
|
||||
/>
|
||||
|
||||
<div className='truncate'>
|
||||
<Text theme='muted' size='sm' truncate>
|
||||
<FormattedMessage
|
||||
id='notification.move'
|
||||
defaultMessage='{name} moved to {targetName}'
|
||||
values={{
|
||||
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
|
||||
targetName: to.acct,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
|
||||
<Account account={to} withRelationship={false} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export { MovedNote as default };
|
||||
@ -13,6 +13,7 @@ import { useFollow } from 'pl-fe/api/hooks/accounts/use-follow';
|
||||
import AltIndicator from 'pl-fe/components/alt-indicator';
|
||||
import Badge from 'pl-fe/components/badge';
|
||||
import DropdownMenu, { Menu } from 'pl-fe/components/dropdown-menu';
|
||||
import Icon from 'pl-fe/components/icon';
|
||||
import StillImage from 'pl-fe/components/still-image';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
@ -21,7 +22,7 @@ import Popover from 'pl-fe/components/ui/popover';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import VerificationBadge from 'pl-fe/components/verification-badge';
|
||||
import MovedNote from 'pl-fe/features/account-timeline/components/moved-note';
|
||||
import Emojify from 'pl-fe/features/emoji/emojify';
|
||||
import ActionButton from 'pl-fe/features/ui/components/action-button';
|
||||
import SubscriptionButton from 'pl-fe/features/ui/components/subscription-button';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
@ -89,6 +90,37 @@ const messages = defineMessages({
|
||||
loadActivitiesFail: { id: 'account.load_activities.fail', defaultMessage: 'Failed to fetch latest posts' },
|
||||
});
|
||||
|
||||
interface IMovedNote {
|
||||
from: AccountEntity;
|
||||
to: AccountEntity;
|
||||
}
|
||||
|
||||
const MovedNote: React.FC<IMovedNote> = ({ from, to }) => (
|
||||
<div className='p-4'>
|
||||
<HStack className='mb-2' alignItems='center' space={1.5}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/outline/briefcase.svg')}
|
||||
className='flex-none text-primary-600 dark:text-primary-400'
|
||||
/>
|
||||
|
||||
<div className='truncate'>
|
||||
<Text theme='muted' size='sm' truncate>
|
||||
<FormattedMessage
|
||||
id='notification.move'
|
||||
defaultMessage='{name} moved to {targetName}'
|
||||
values={{
|
||||
name: <span><Emojify text={from.display_name} emojis={from.emojis} /></span>,
|
||||
targetName: to.acct,
|
||||
}}
|
||||
/>
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
|
||||
<Account account={to} withRelationship={false} />
|
||||
</div>
|
||||
);
|
||||
|
||||
interface IHeader {
|
||||
account?: Account;
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import { useFollowedTags } from 'pl-fe/queries/hashtags/use-followed-tags';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
});
|
||||
|
||||
const FollowedTags = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data: tags = [], isLoading, hasNextPage, fetchNextPage } = useFollowedTags();
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage="You haven't followed any hashtag yet." />;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='followedTags'
|
||||
emptyMessage={emptyMessage}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasNextPage}
|
||||
onLoadMore={fetchNextPage}
|
||||
placeholderComponent={PlaceholderHashtag}
|
||||
placeholderCount={5}
|
||||
itemClassName='pb-3'
|
||||
>
|
||||
{tags.map(tag => <Hashtag key={tag.name} hashtag={tag} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { FollowedTags as default };
|
||||
@ -1,103 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { type FilterType, setFilter } from 'pl-fe/actions/notifications';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Tabs from 'pl-fe/components/ui/tabs';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
|
||||
import type { Item } from 'pl-fe/components/ui/tabs';
|
||||
|
||||
const messages = defineMessages({
|
||||
all: { id: 'notifications.filter.all', defaultMessage: 'All' },
|
||||
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
|
||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
|
||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||
events: { id: 'notifications.filter.events', defaultMessage: 'Events' },
|
||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
});
|
||||
|
||||
const NotificationFilterBar = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
const features = useFeatures();
|
||||
|
||||
const selectedFilter = settings.notifications.quickFilter.active;
|
||||
const advancedMode = settings.notifications.quickFilter.advanced;
|
||||
|
||||
const onClick = (notificationType: FilterType) => () => {
|
||||
try {
|
||||
dispatch(setFilter(notificationType, true));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
text: intl.formatMessage(messages.all),
|
||||
action: onClick('all'),
|
||||
name: 'all',
|
||||
},
|
||||
];
|
||||
|
||||
if (!advancedMode) {
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.mentions),
|
||||
action: onClick('mention'),
|
||||
name: 'mention',
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/at.svg')} />,
|
||||
title: intl.formatMessage(messages.mentions),
|
||||
action: onClick('mention'),
|
||||
name: 'mention',
|
||||
});
|
||||
if (features.accountNotifies) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/bell-ringing.svg')} />,
|
||||
title: intl.formatMessage(messages.statuses),
|
||||
action: onClick('status'),
|
||||
name: 'status',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/star.svg')} />,
|
||||
title: intl.formatMessage(messages.favourites),
|
||||
action: onClick('favourite'),
|
||||
name: 'favourite',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/repeat.svg')} />,
|
||||
title: intl.formatMessage(messages.boosts),
|
||||
action: onClick('reblog'),
|
||||
name: 'reblog',
|
||||
});
|
||||
if (features.polls) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/chart-bar.svg')} />,
|
||||
title: intl.formatMessage(messages.polls),
|
||||
action: onClick('poll'),
|
||||
name: 'poll',
|
||||
});
|
||||
if (features.events) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/calendar.svg')} />,
|
||||
title: intl.formatMessage(messages.events),
|
||||
action: onClick('events'),
|
||||
name: 'events',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/user-plus.svg')} />,
|
||||
title: intl.formatMessage(messages.follows),
|
||||
action: onClick('follow'),
|
||||
name: 'follow',
|
||||
});
|
||||
}
|
||||
|
||||
return <Tabs items={items} activeItem={selectedFilter} />;
|
||||
};
|
||||
|
||||
export { NotificationFilterBar as default };
|
||||
@ -1,95 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
const Search = () => {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [value, setValue] = useState(params.get('q') || '');
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const setQuery = (value: string) => {
|
||||
setParams(params => ({ ...Object.fromEntries(params.entries()), q: value }));
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (params.get('q') === value) {
|
||||
if (value.length > 0) {
|
||||
setValue('');
|
||||
setQuery('');
|
||||
}
|
||||
} else {
|
||||
setQuery(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
setQuery(value);
|
||||
} else if (event.key === 'Escape') {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className='sticky top-[76px] z-10 w-full bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90'
|
||||
>
|
||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||
|
||||
<div className='relative'>
|
||||
<Input
|
||||
type='text'
|
||||
id='search'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
theme='search'
|
||||
className='pr-10 rtl:pl-10 rtl:pr-3'
|
||||
/>
|
||||
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto'
|
||||
onClick={handleClick}
|
||||
>
|
||||
{params.get('q') === value ? (
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/outline/x.svg')}
|
||||
className='size-4 text-gray-600'
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
) : (
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/outline/search.svg')}
|
||||
className='size-4 text-gray-600'
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { Search as default };
|
||||
@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Search from 'pl-fe/features/search/components/search';
|
||||
import SearchResults from 'pl-fe/features/search/components/search-results';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.search', defaultMessage: 'Search' },
|
||||
});
|
||||
|
||||
const SearchPage = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<div className='space-y-4'>
|
||||
<Search />
|
||||
<SearchResults />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchPage as default };
|
||||
@ -1,17 +1,22 @@
|
||||
import { lazy } from 'react';
|
||||
|
||||
// Pages
|
||||
export const AccountGallery = lazy(() => import('pl-fe/pages/accounts/account-gallery'));
|
||||
export const AccountTimeline = lazy(() => import('pl-fe/pages/accounts/account-timeline'));
|
||||
export const AboutPage = lazy(() => import('pl-fe/pages/utils/about'));
|
||||
export const Aliases = lazy(() => import('pl-fe/pages/settings/aliases'));
|
||||
export const Announcements = lazy(() => import('pl-fe/pages/dashboard/announcements'));
|
||||
export const AuthTokenList = lazy(() => import('pl-fe/pages/settings/auth-token-list'));
|
||||
export const Backups = lazy(() => import('pl-fe/pages/settings/backups'));
|
||||
export const Blocks = lazy(() => import('pl-fe/pages/settings/blocks'));
|
||||
export const BookmarkFolders = lazy(() => import('pl-fe/pages/status-lists/bookmark-folders'));
|
||||
export const Bookmarks = lazy(() => import('pl-fe/pages/status-lists/bookmarks'));
|
||||
export const BubbleTimeline = lazy(() => import('pl-fe/pages/timelines/bubble-timeline'));
|
||||
export const ChatIndex = lazy(() => import('pl-fe/pages/chats/chats'));
|
||||
export const Circle = lazy(() => import('pl-fe/pages/fun/circle'));
|
||||
export const Circles = lazy(() => import('pl-fe/pages/account-lists/circles'));
|
||||
export const CommunityTimeline = lazy(() => import('pl-fe/pages/timelines/community-timeline'));
|
||||
export const ComposeEvent = lazy(() => import('pl-fe/pages/statuses/compose-event'));
|
||||
export const Conversations = lazy(() => import('pl-fe/pages/status-lists/conversations'));
|
||||
export const CreateApp = lazy(() => import('pl-fe/pages/developers/create-app'));
|
||||
export const CryptoDonate = lazy(() => import('pl-fe/pages/utils/crypto-donate'));
|
||||
@ -21,16 +26,21 @@ export const Developers = lazy(() => import('pl-fe/pages/developers/developers')
|
||||
export const Directory = lazy(() => import('pl-fe/pages/account-lists/directory'));
|
||||
export const DomainBlocks = lazy(() => import('pl-fe/pages/settings/domain-blocks'));
|
||||
export const Domains = lazy(() => import('pl-fe/pages/dashboard/domains'));
|
||||
export const DraftStatuses = lazy(() => import('pl-fe/pages/status-lists/draft-statuses'));
|
||||
export const EditEmail = lazy(() => import('pl-fe/pages/settings/edit-email'));
|
||||
export const EditFilter = lazy(() => import('pl-fe/pages/settings/edit-filter'));
|
||||
export const EditGroup = lazy(() => import('pl-fe/pages/groups/edit-group'));
|
||||
export const EditPassword = lazy(() => import('pl-fe/pages/settings/edit-password'));
|
||||
export const EditProfile = lazy(() => import('pl-fe/pages/settings/edit-profile'));
|
||||
export const EventDiscussion = lazy(() => import('pl-fe/pages/statuses/event-discussion'));
|
||||
export const EventInformation = lazy(() => import('pl-fe/pages/statuses/event-information'));
|
||||
export const Events = lazy(() => import('pl-fe/pages/status-lists/events'));
|
||||
export const ExportData = lazy(() => import('pl-fe/pages/settings/export-data'));
|
||||
export const ExternalLogin = lazy(() => import('pl-fe/pages/auth/external-login'));
|
||||
export const FavouritedStatuses = lazy(() => import('pl-fe/pages/status-lists/favourited-statuses'));
|
||||
export const FederationRestrictions = lazy(() => import('pl-fe/pages/utils/federation-restrictions'));
|
||||
export const Filters = lazy(() => import('pl-fe/pages/settings'));
|
||||
export const Filters = lazy(() => import('pl-fe/pages/settings/filters'));
|
||||
export const FollowedTags = lazy(() => import('pl-fe/pages/settings'));
|
||||
export const Followers = lazy(() => import('pl-fe/pages/account-lists/followers'));
|
||||
export const Following = lazy(() => import('pl-fe/pages/account-lists/following'));
|
||||
export const FollowRecommendations = lazy(() => import('pl-fe/pages/account-lists/follow-recommendations'));
|
||||
@ -47,6 +57,7 @@ export const HomeTimeline = lazy(() => import('pl-fe/pages/timelines/home-timeli
|
||||
export const ImportData = lazy(() => import('pl-fe/pages/settings/import-data'));
|
||||
export const IntentionalError = lazy(() => import('pl-fe/pages/utils/intentional-error'));
|
||||
export const InteractionPolicies = lazy(() => import('pl-fe/pages/settings/interaction-policies'));
|
||||
export const InteractionRequests = lazy(() => import('pl-fe/pages/status-lists/interaction-requests'));
|
||||
export const LandingPage = lazy(() => import('pl-fe/pages/utils/landing'));
|
||||
export const LandingTimeline = lazy(() => import('pl-fe/pages/timelines/landing-timeline'));
|
||||
export const LinkTimeline = lazy(() => import('pl-fe/pages/timelines/link-timeline'));
|
||||
@ -60,6 +71,7 @@ export const Migration = lazy(() => import('pl-fe/pages/settings/migration'));
|
||||
export const ModerationLog = lazy(() => import('pl-fe/pages/dashboard/moderation-log'));
|
||||
export const Mutes = lazy(() => import('pl-fe/pages/settings/mutes'));
|
||||
export const NewStatus = lazy(() => import('pl-fe/pages/utils/new-status'));
|
||||
export const Notifications = lazy(() => import('pl-fe/pages/notifications/notifications'));
|
||||
export const OutgoingFollowRequests = lazy(() => import('pl-fe/pages/account-lists/outgoing-follow-requests'));
|
||||
export const PasswordReset = lazy(() => import('pl-fe/pages/auth/password-reset'));
|
||||
export const PinnedStatuses = lazy(() => import('pl-fe/pages/status-lists/pinned-statuses'));
|
||||
@ -71,34 +83,19 @@ export const RegistrationPage = lazy(() => import('pl-fe/pages/auth/registration
|
||||
export const Relays = lazy(() => import('pl-fe/pages/dashboard/relays'));
|
||||
export const RemoteTimeline = lazy(() => import('pl-fe/pages/timelines/remote-timeline'));
|
||||
export const Rules = lazy(() => import('pl-fe/pages/dashboard/rules'));
|
||||
export const ScheduledStatuses = lazy(() => import('pl-fe/pages/status-lists/scheduled-statuses'));
|
||||
export const Search = lazy(() => import('pl-fe/pages/search/search'));
|
||||
export const ServiceWorkerInfo = lazy(() => import('pl-fe/pages/developers/service-worker-info'));
|
||||
export const ServerInfo = lazy(() => import('pl-fe/pages/utils/server-info'));
|
||||
export const Settings = lazy(() => import('pl-fe/pages/settings/settings'));
|
||||
export const SettingsStore = lazy(() => import('pl-fe/pages/developers/settings-store'));
|
||||
export const Share = lazy(() => import('pl-fe/pages/utils/share'));
|
||||
export const Status = lazy(() => import('pl-fe/pages/statuses/status'));
|
||||
export const TestTimeline = lazy(() => import('pl-fe/pages/timelines/test-timeline'));
|
||||
export const ThemeEditor = lazy(() => import('pl-fe/pages/dashboard/theme-editor'));
|
||||
export const UrlPrivacy = lazy(() => import('pl-fe/pages/settings/url-privacy'));
|
||||
export const UserIndex = lazy(() => import('pl-fe/pages/dashboard/user-index'));
|
||||
|
||||
export const DraftStatuses = lazy(() => import('pl-fe/pages/status-lists/draft-statuses'));
|
||||
export const EventDiscussion = lazy(() => import('pl-fe/pages/statuses/event-discussion'));
|
||||
export const EventInformation = lazy(() => import('pl-fe/pages/statuses/event-information'));
|
||||
export const Events = lazy(() => import('pl-fe/pages/status-lists/events'));
|
||||
export const InteractionRequests = lazy(() => import('pl-fe/pages/status-lists/interaction-requests'));
|
||||
export const ScheduledStatuses = lazy(() => import('pl-fe/pages/status-lists/scheduled-statuses'));
|
||||
export const Status = lazy(() => import('pl-fe/pages/statuses/status'));
|
||||
|
||||
export const AccountGallery = lazy(() => import('pl-fe/features/account-gallery'));
|
||||
export const AccountTimeline = lazy(() => import('pl-fe/features/account-timeline'));
|
||||
export const BookmarkFolders = lazy(() => import('pl-fe/features/bookmark-folders'));
|
||||
export const Circle = lazy(() => import('pl-fe/features/circle'));
|
||||
export const ComposeEditor = lazy(() => import('pl-fe/features/compose/editor'));
|
||||
export const ComposeEvent = lazy(() => import('pl-fe/features/compose-event'));
|
||||
export const FollowedTags = lazy(() => import('pl-fe/features/followed-tags'));
|
||||
export const Notifications = lazy(() => import('pl-fe/features/notifications'));
|
||||
export const Search = lazy(() => import('pl-fe/features/search'));
|
||||
|
||||
// Panels
|
||||
export const AccountNotePanel = lazy(() => import('pl-fe/features/ui/components/panels/account-note-panel'));
|
||||
export const AnnouncementsPanel = lazy(() => import('pl-fe/components/announcements/announcements-panel'));
|
||||
@ -123,6 +120,7 @@ export const WhoToFollowPanel = lazy(() => import('pl-fe/features/ui/components/
|
||||
|
||||
export const Audio = lazy(() => import('pl-fe/features/audio'));
|
||||
export const ChatWidget = lazy(() => import('pl-fe/features/chats/components/chat-widget/chat-widget'));
|
||||
export const ComposeEditor = lazy(() => import('pl-fe/features/compose/editor'));
|
||||
export const ComposeForm = lazy(() => import('pl-fe/features/compose/components/compose-form'));
|
||||
export const CryptoAddress = lazy(() => import('pl-fe/features/crypto-donate/components/crypto-address'));
|
||||
export const CryptoIcon = lazy(() => import('pl-fe/features/crypto-donate/components/crypto-icon'));
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import { useAccountLookup } from 'pl-fe/api/hooks/accounts/use-account-lookup';
|
||||
import Blurhash from 'pl-fe/components/blurhash';
|
||||
import Icon from 'pl-fe/components/icon';
|
||||
import LoadMore from 'pl-fe/components/load-more';
|
||||
import MissingIndicator from 'pl-fe/components/missing-indicator';
|
||||
import StillImage from 'pl-fe/components/still-image';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Spinner from 'pl-fe/components/ui/spinner';
|
||||
import { type AccountGalleryAttachment, useAccountGallery } from 'pl-fe/hooks/use-account-gallery';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
import { isIOS } from 'pl-fe/is-mobile';
|
||||
|
||||
import type { AccountGalleryAttachment } from 'pl-fe/hooks/use-account-gallery';
|
||||
import { useModalsStore } from 'pl-fe/stores/modals';
|
||||
|
||||
interface IMediaItem {
|
||||
attachment: AccountGalleryAttachment;
|
||||
@ -135,4 +141,99 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, onOpenMedia, isLast }) =>
|
||||
);
|
||||
};
|
||||
|
||||
export { MediaItem as default };
|
||||
const AccountGalleryPage = () => {
|
||||
const { username } = useParams<{ username: string }>();
|
||||
const { openModal } = useModalsStore();
|
||||
|
||||
const {
|
||||
account,
|
||||
isLoading: accountLoading,
|
||||
isUnavailable,
|
||||
} = useAccountLookup(username, { withRelationship: true });
|
||||
|
||||
const { data: attachments, isFetching, isLoading, hasNextPage: hasMore, fetchNextPage } = useAccountGallery(account?.id!);
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
if (hasMore) {
|
||||
handleLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoadMore = () => {
|
||||
fetchNextPage({ cancelRefetch: false });
|
||||
};
|
||||
|
||||
const handleLoadOlder: React.MouseEventHandler = e => {
|
||||
e.preventDefault();
|
||||
handleScrollToBottom();
|
||||
};
|
||||
|
||||
const handleOpenMedia = (attachment: AccountGalleryAttachment) => {
|
||||
if (attachment.type === 'video') {
|
||||
openModal('VIDEO', { media: attachment, statusId: attachment.status_id });
|
||||
} else {
|
||||
openModal('MEDIA', { index: attachment.index, statusId: attachment.status_id });
|
||||
}
|
||||
};
|
||||
|
||||
if (accountLoading || isLoading) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
if (!account) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
let loadOlder = null;
|
||||
|
||||
if (hasMore && !(isFetching && attachments.length === 0)) {
|
||||
loadOlder = <LoadMore className='my-auto mt-4' visible={!isFetching} onClick={handleLoadOlder} />;
|
||||
}
|
||||
|
||||
if (isUnavailable) {
|
||||
return (
|
||||
<Column>
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={`@${account.acct}`} transparent withHeader={false}>
|
||||
<div role='feed' className='grid grid-cols-2 gap-1 overflow-hidden rounded-md sm:grid-cols-3'>
|
||||
{attachments.map((attachment, index) => (
|
||||
<MediaItem
|
||||
key={`${attachment.status_id}+${attachment.id}`}
|
||||
attachment={attachment}
|
||||
onOpenMedia={handleOpenMedia}
|
||||
isLast={index === attachments.length - 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!isLoading && attachments.length === 0 && (
|
||||
<div className='empty-column-indicator col-span-2 sm:col-span-3'>
|
||||
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loadOlder}
|
||||
|
||||
{isFetching && attachments.length === 0 && (
|
||||
<div className='relative flex-auto px-8 py-4'>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountGalleryPage as default };
|
||||
@ -18,14 +18,14 @@ import { makeGetStatusIds } from 'pl-fe/selectors';
|
||||
|
||||
const getStatusIds = makeGetStatusIds();
|
||||
|
||||
interface IAccountTimeline {
|
||||
interface IAccountTimelinePage {
|
||||
params: {
|
||||
username: string;
|
||||
};
|
||||
withReplies?: boolean;
|
||||
}
|
||||
|
||||
const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = false }) => {
|
||||
const AccountTimelinePage: React.FC<IAccountTimelinePage> = ({ params, withReplies = false }) => {
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
@ -103,4 +103,4 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
||||
);
|
||||
};
|
||||
|
||||
export { AccountTimeline as default };
|
||||
export { AccountTimelinePage as default };
|
||||
@ -36,7 +36,7 @@ const messages = defineMessages({
|
||||
done: { id: 'interactions_circle.state.done', defaultMessage: 'Finalizing…' },
|
||||
});
|
||||
|
||||
const Circle: React.FC = () => {
|
||||
const CirclePage: React.FC = () => {
|
||||
const [{ state, progress }, setProgress] = useState<{
|
||||
state: 'unrequested' | 'pending' | 'fetchingStatuses' | 'fetchingFavourites' | 'fetchingAvatars' | 'drawing' | 'done';
|
||||
progress: number;
|
||||
@ -220,4 +220,4 @@ const Circle: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export { Circle as default };
|
||||
export { CirclePage as default };
|
||||
@ -5,32 +5,123 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import {
|
||||
type FilterType,
|
||||
expandNotifications,
|
||||
markReadNotifications,
|
||||
scrollTopNotifications,
|
||||
setFilter,
|
||||
} from 'pl-fe/actions/notifications';
|
||||
import PullToRefresh from 'pl-fe/components/pull-to-refresh';
|
||||
import ScrollTopButton from 'pl-fe/components/scroll-top-button';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import Icon from 'pl-fe/components/ui/icon';
|
||||
import Portal from 'pl-fe/components/ui/portal';
|
||||
import Tabs from 'pl-fe/components/ui/tabs';
|
||||
import PlaceholderNotification from 'pl-fe/features/placeholder/components/placeholder-notification';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useSettings } from 'pl-fe/hooks/use-settings';
|
||||
|
||||
import FilterBar from './components/filter-bar';
|
||||
import Notification from './components/notification';
|
||||
import Notification from '../../features/notifications/components/notification';
|
||||
|
||||
import type { Item } from 'pl-fe/components/ui/tabs';
|
||||
import type { RootState } from 'pl-fe/store';
|
||||
import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' },
|
||||
all: { id: 'notifications.filter.all', defaultMessage: 'All' },
|
||||
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
|
||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
|
||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||
events: { id: 'notifications.filter.events', defaultMessage: 'Events' },
|
||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
});
|
||||
|
||||
const FilterBar = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
const features = useFeatures();
|
||||
|
||||
const selectedFilter = settings.notifications.quickFilter.active;
|
||||
const advancedMode = settings.notifications.quickFilter.advanced;
|
||||
|
||||
const onClick = (notificationType: FilterType) => () => {
|
||||
try {
|
||||
dispatch(setFilter(notificationType, true));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
text: intl.formatMessage(messages.all),
|
||||
action: onClick('all'),
|
||||
name: 'all',
|
||||
},
|
||||
];
|
||||
|
||||
if (!advancedMode) {
|
||||
items.push({
|
||||
text: intl.formatMessage(messages.mentions),
|
||||
action: onClick('mention'),
|
||||
name: 'mention',
|
||||
});
|
||||
} else {
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/at.svg')} />,
|
||||
title: intl.formatMessage(messages.mentions),
|
||||
action: onClick('mention'),
|
||||
name: 'mention',
|
||||
});
|
||||
if (features.accountNotifies) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/bell-ringing.svg')} />,
|
||||
title: intl.formatMessage(messages.statuses),
|
||||
action: onClick('status'),
|
||||
name: 'status',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/star.svg')} />,
|
||||
title: intl.formatMessage(messages.favourites),
|
||||
action: onClick('favourite'),
|
||||
name: 'favourite',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/repeat.svg')} />,
|
||||
title: intl.formatMessage(messages.boosts),
|
||||
action: onClick('reblog'),
|
||||
name: 'reblog',
|
||||
});
|
||||
if (features.polls) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/chart-bar.svg')} />,
|
||||
title: intl.formatMessage(messages.polls),
|
||||
action: onClick('poll'),
|
||||
name: 'poll',
|
||||
});
|
||||
if (features.events) items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/calendar.svg')} />,
|
||||
title: intl.formatMessage(messages.events),
|
||||
action: onClick('events'),
|
||||
name: 'events',
|
||||
});
|
||||
items.push({
|
||||
text: <Icon className='size-4' src={require('@tabler/icons/outline/user-plus.svg')} />,
|
||||
title: intl.formatMessage(messages.follows),
|
||||
action: onClick('follow'),
|
||||
name: 'follow',
|
||||
});
|
||||
}
|
||||
|
||||
return <Tabs items={items} activeItem={selectedFilter} />;
|
||||
};
|
||||
|
||||
const getNotifications = createSelector([
|
||||
(state: RootState) => state.notifications.items,
|
||||
(_, topNotification?: string) => topNotification,
|
||||
@ -53,7 +144,7 @@ const getNotifications = createSelector([
|
||||
};
|
||||
});
|
||||
|
||||
const Notifications = () => {
|
||||
const NotificationsPage = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const features = useFeatures();
|
||||
const intl = useIntl();
|
||||
@ -208,4 +299,4 @@ const Notifications = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export { Notifications as default };
|
||||
export { NotificationsPage as default };
|
||||
@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
@ -8,7 +8,10 @@ import Hashtag from 'pl-fe/components/hashtag';
|
||||
import IconButton from 'pl-fe/components/icon-button';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import TrendingLink from 'pl-fe/components/trending-link';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import SvgIcon from 'pl-fe/components/ui/svg-icon';
|
||||
import Tabs from 'pl-fe/components/ui/tabs';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import AccountContainer from 'pl-fe/containers/account-container';
|
||||
@ -28,12 +31,97 @@ import type { VirtuosoHandle } from 'react-virtuoso';
|
||||
type SearchFilter = 'accounts' | 'hashtags' | 'statuses' | 'links';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.search', defaultMessage: 'Search' },
|
||||
placeholder: { id: 'search.placeholder', defaultMessage: 'Search' },
|
||||
accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
|
||||
statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
|
||||
hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
|
||||
links: { id: 'search_results.links', defaultMessage: 'News' },
|
||||
});
|
||||
|
||||
const SearchInput = () => {
|
||||
const [params, setParams] = useSearchParams();
|
||||
const [value, setValue] = useState(params.get('q') || '');
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
const setQuery = (value: string) => {
|
||||
setParams(params => ({ ...Object.fromEntries(params.entries()), q: value }));
|
||||
};
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (params.get('q') === value) {
|
||||
if (value.length > 0) {
|
||||
setValue('');
|
||||
setQuery('');
|
||||
}
|
||||
} else {
|
||||
setQuery(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
|
||||
setQuery(value);
|
||||
} else if (event.key === 'Escape') {
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className='sticky top-[76px] z-10 w-full bg-white/90 backdrop-blur black:bg-black/80 dark:bg-primary-900/90'
|
||||
>
|
||||
<label htmlFor='search' className='sr-only'>{intl.formatMessage(messages.placeholder)}</label>
|
||||
|
||||
<div className='relative'>
|
||||
<Input
|
||||
type='text'
|
||||
id='search'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
autoFocus
|
||||
theme='search'
|
||||
className='pr-10 rtl:pl-10 rtl:pr-3'
|
||||
/>
|
||||
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto'
|
||||
onClick={handleClick}
|
||||
>
|
||||
{params.get('q') === value ? (
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/outline/x.svg')}
|
||||
className='size-4 text-gray-600'
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
) : (
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/outline/search.svg')}
|
||||
className='size-4 text-gray-600'
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SearchResults = () => {
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
|
||||
@ -274,4 +362,17 @@ const SearchResults = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchResults as default };
|
||||
const SearchPage = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<div className='space-y-4'>
|
||||
<SearchInput />
|
||||
<SearchResults />
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { SearchPage as default };
|
||||
126
packages/pl-fe/src/pages/settings/filters.tsx
Normal file
126
packages/pl-fe/src/pages/settings/filters.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { fetchFilters, deleteFilter } from 'pl-fe/actions/filters';
|
||||
import RelativeTimestamp from 'pl-fe/components/relative-timestamp';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import toast from 'pl-fe/toast';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
||||
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
|
||||
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
|
||||
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
|
||||
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
|
||||
accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' },
|
||||
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
|
||||
edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' },
|
||||
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
const contexts = {
|
||||
home: messages.home_timeline,
|
||||
public: messages.public_timeline,
|
||||
notifications: messages.notifications,
|
||||
thread: messages.conversations,
|
||||
account: messages.accounts,
|
||||
};
|
||||
|
||||
const FiltersPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const { filtersV2 } = useFeatures();
|
||||
|
||||
const filters = useAppSelector((state) => state.filters);
|
||||
|
||||
const handleFilterEdit = (id: string) => () => history.push(`/filters/${id}`);
|
||||
|
||||
const handleFilterDelete = (id: string) => () => {
|
||||
dispatch(deleteFilter(id)).then(() => dispatch(fetchFilters())).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.delete_error));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchFilters());
|
||||
}, []);
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
||||
|
||||
return (
|
||||
<Column className='filter-settings-panel' label={intl.formatMessage(messages.heading)}>
|
||||
<HStack className='mb-4' space={2} justifyContent='end'>
|
||||
<Button
|
||||
to='/filters/new'
|
||||
theme='primary'
|
||||
size='sm'
|
||||
>
|
||||
<FormattedMessage id='filters.create_filter' defaultMessage='Create filter' />
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
>
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||
<Stack space={2}>
|
||||
<Stack className='grow' space={1}>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_phrases_label' defaultMessage='Keywords or phrases:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.keywords.map(keyword => keyword.keyword).join(', ')}</Text>
|
||||
</Text>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')}</Text>
|
||||
</Text>
|
||||
<HStack space={4} wrap>
|
||||
<Text weight='medium'>
|
||||
{filtersV2 ? (
|
||||
filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_hide_completely' defaultMessage='Hide content' /> :
|
||||
<FormattedMessage id='filters.filters_list_warn' defaultMessage='Display warning' />
|
||||
) : (filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /> :
|
||||
<FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' />)}
|
||||
</Text>
|
||||
{filter.expires_at && (
|
||||
<Text weight='medium'>
|
||||
{new Date(filter.expires_at).getTime() <= Date.now()
|
||||
? <FormattedMessage id='filters.filters_list_expired' defaultMessage='Expired' />
|
||||
: <RelativeTimestamp timestamp={filter.expires_at} className='whitespace-nowrap' futureDate />}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<HStack space={2} justifyContent='end'>
|
||||
<Button theme='primary' onClick={handleFilterEdit(filter.id)}>
|
||||
{intl.formatMessage(messages.edit)}
|
||||
</Button>
|
||||
<Button theme='danger' onClick={handleFilterDelete(filter.id)}>
|
||||
{intl.formatMessage(messages.delete)}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { FiltersPage as default };
|
||||
@ -1,126 +1,39 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { fetchFilters, deleteFilter } from 'pl-fe/actions/filters';
|
||||
import RelativeTimestamp from 'pl-fe/components/relative-timestamp';
|
||||
import Hashtag from 'pl-fe/components/hashtag';
|
||||
import ScrollableList from 'pl-fe/components/scrollable-list';
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Column from 'pl-fe/components/ui/column';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Stack from 'pl-fe/components/ui/stack';
|
||||
import Text from 'pl-fe/components/ui/text';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import toast from 'pl-fe/toast';
|
||||
import PlaceholderHashtag from 'pl-fe/features/placeholder/components/placeholder-hashtag';
|
||||
import { useFollowedTags } from 'pl-fe/queries/hashtags/use-followed-tags';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.filters', defaultMessage: 'Muted words' },
|
||||
home_timeline: { id: 'column.filters.home_timeline', defaultMessage: 'Home timeline' },
|
||||
public_timeline: { id: 'column.filters.public_timeline', defaultMessage: 'Public timeline' },
|
||||
notifications: { id: 'column.filters.notifications', defaultMessage: 'Notifications' },
|
||||
conversations: { id: 'column.filters.conversations', defaultMessage: 'Conversations' },
|
||||
accounts: { id: 'column.filters.accounts', defaultMessage: 'Accounts' },
|
||||
delete_error: { id: 'column.filters.delete_error', defaultMessage: 'Error deleting filter' },
|
||||
edit: { id: 'column.filters.edit', defaultMessage: 'Edit filter' },
|
||||
delete: { id: 'column.filters.delete', defaultMessage: 'Delete' },
|
||||
heading: { id: 'column.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
});
|
||||
|
||||
const contexts = {
|
||||
home: messages.home_timeline,
|
||||
public: messages.public_timeline,
|
||||
notifications: messages.notifications,
|
||||
thread: messages.conversations,
|
||||
account: messages.accounts,
|
||||
};
|
||||
|
||||
const FiltersPage = () => {
|
||||
const FollowedTagsPage = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const { filtersV2 } = useFeatures();
|
||||
|
||||
const filters = useAppSelector((state) => state.filters);
|
||||
const { data: tags = [], isLoading, hasNextPage, fetchNextPage } = useFollowedTags();
|
||||
|
||||
const handleFilterEdit = (id: string) => () => history.push(`/filters/${id}`);
|
||||
|
||||
const handleFilterDelete = (id: string) => () => {
|
||||
dispatch(deleteFilter(id)).then(() => dispatch(fetchFilters())).catch(() => {
|
||||
toast.error(intl.formatMessage(messages.delete_error));
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchFilters());
|
||||
}, []);
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.filters' defaultMessage="You haven't created any muted words yet." />;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage="You haven't followed any hashtag yet." />;
|
||||
|
||||
return (
|
||||
<Column className='filter-settings-panel' label={intl.formatMessage(messages.heading)}>
|
||||
<HStack className='mb-4' space={2} justifyContent='end'>
|
||||
<Button
|
||||
to='/filters/new'
|
||||
theme='primary'
|
||||
size='sm'
|
||||
>
|
||||
<FormattedMessage id='filters.create_filter' defaultMessage='Create filter' />
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
scrollKey='filters'
|
||||
scrollKey='followedTags'
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='pb-4 last:pb-0'
|
||||
isLoading={isLoading}
|
||||
hasMore={hasNextPage}
|
||||
onLoadMore={fetchNextPage}
|
||||
placeholderComponent={PlaceholderHashtag}
|
||||
placeholderCount={5}
|
||||
itemClassName='pb-3'
|
||||
>
|
||||
{filters.map((filter) => (
|
||||
<div key={filter.id} className='rounded-lg bg-gray-100 p-4 dark:bg-primary-800'>
|
||||
<Stack space={2}>
|
||||
<Stack className='grow' space={1}>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_phrases_label' defaultMessage='Keywords or phrases:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.keywords.map(keyword => keyword.keyword).join(', ')}</Text>
|
||||
</Text>
|
||||
<Text weight='medium'>
|
||||
<FormattedMessage id='filters.filters_list_context_label' defaultMessage='Filter contexts:' />
|
||||
{' '}
|
||||
<Text theme='muted' tag='span'>{filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')}</Text>
|
||||
</Text>
|
||||
<HStack space={4} wrap>
|
||||
<Text weight='medium'>
|
||||
{filtersV2 ? (
|
||||
filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_hide_completely' defaultMessage='Hide content' /> :
|
||||
<FormattedMessage id='filters.filters_list_warn' defaultMessage='Display warning' />
|
||||
) : (filter.filter_action === 'hide' ?
|
||||
<FormattedMessage id='filters.filters_list_drop' defaultMessage='Drop' /> :
|
||||
<FormattedMessage id='filters.filters_list_hide' defaultMessage='Hide' />)}
|
||||
</Text>
|
||||
{filter.expires_at && (
|
||||
<Text weight='medium'>
|
||||
{new Date(filter.expires_at).getTime() <= Date.now()
|
||||
? <FormattedMessage id='filters.filters_list_expired' defaultMessage='Expired' />
|
||||
: <RelativeTimestamp timestamp={filter.expires_at} className='whitespace-nowrap' futureDate />}
|
||||
</Text>
|
||||
)}
|
||||
</HStack>
|
||||
</Stack>
|
||||
<HStack space={2} justifyContent='end'>
|
||||
<Button theme='primary' onClick={handleFilterEdit(filter.id)}>
|
||||
{intl.formatMessage(messages.edit)}
|
||||
</Button>
|
||||
<Button theme='danger' onClick={handleFilterDelete(filter.id)}>
|
||||
{intl.formatMessage(messages.delete)}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</div>
|
||||
))}
|
||||
{tags.map(tag => <Hashtag key={tag.name} hashtag={tag} />)}
|
||||
</ScrollableList>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { FiltersPage as default };
|
||||
export { FollowedTagsPage as default };
|
||||
|
||||
@ -73,7 +73,7 @@ const NewFolderForm: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const BookmarkFolders: React.FC = () => {
|
||||
const BookmarkFoldersPage: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const features = useFeatures();
|
||||
|
||||
@ -128,4 +128,4 @@ const BookmarkFolders: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export { BookmarkFolders as default, NewFolderForm };
|
||||
export { BookmarkFoldersPage as default, NewFolderForm };
|
||||
@ -7,8 +7,8 @@ import Stack from 'pl-fe/components/ui/stack';
|
||||
import Tabs from 'pl-fe/components/ui/tabs';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
|
||||
import { EditEvent } from './tabs/edit-event';
|
||||
import { ManagePendingParticipants } from './tabs/manage-pending-participants';
|
||||
import { EditEvent } from '../../features/compose-event/tabs/edit-event';
|
||||
import { ManagePendingParticipants } from '../../features/compose-event/tabs/manage-pending-participants';
|
||||
|
||||
const messages = defineMessages({
|
||||
manageEvent: { id: 'navigation_bar.manage_event', defaultMessage: 'Manage event' },
|
||||
@ -21,11 +21,11 @@ type RouteParams = {
|
||||
statusId?: string;
|
||||
};
|
||||
|
||||
interface IComposeEvent {
|
||||
interface IComposeEventPage {
|
||||
params: RouteParams;
|
||||
}
|
||||
|
||||
const ComposeEvent: React.FC<IComposeEvent> = ({ params }) => {
|
||||
const ComposeEventPage: React.FC<IComposeEventPage> = ({ params }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -64,4 +64,4 @@ const ComposeEvent: React.FC<IComposeEvent> = ({ params }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export { ComposeEvent as default };
|
||||
export { ComposeEventPage as default };
|
||||
Reference in New Issue
Block a user