From 31019121cbac480d864d86d8557120165fecc4de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicole=20Miko=C5=82ajczyk?= Date: Fri, 16 May 2025 20:11:27 +0200 Subject: [PATCH] pl-fe: move files around MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Nicole Mikołajczyk --- .../src/features/account-gallery/index.tsx | 110 --------------- .../components/moved-note.tsx | 43 ------ .../features/account/components/header.tsx | 34 ++++- .../src/features/followed-tags/index.tsx | 39 ------ .../notifications/components/filter-bar.tsx | 103 -------------- ...cation.test.tsx => notifications.test.tsx} | 0 .../src/features/search/components/search.tsx | 95 ------------- packages/pl-fe/src/features/search/index.tsx | 25 ---- .../src/features/ui/util/async-components.ts | 36 +++-- .../accounts/account-gallery.tsx} | 109 ++++++++++++++- .../accounts/account-timeline.tsx} | 6 +- .../circle/index.tsx => pages/fun/circle.tsx} | 4 +- .../notifications/notifications.tsx} | 99 +++++++++++++- .../search/search.tsx} | 105 ++++++++++++++- packages/pl-fe/src/pages/settings/filters.tsx | 126 ++++++++++++++++++ packages/pl-fe/src/pages/settings/index.tsx | 125 +++-------------- .../status-lists/bookmark-folders.tsx} | 4 +- .../statuses/compose-event.tsx} | 10 +- 18 files changed, 510 insertions(+), 563 deletions(-) delete mode 100644 packages/pl-fe/src/features/account-gallery/index.tsx delete mode 100644 packages/pl-fe/src/features/account-timeline/components/moved-note.tsx delete mode 100644 packages/pl-fe/src/features/followed-tags/index.tsx delete mode 100644 packages/pl-fe/src/features/notifications/components/filter-bar.tsx rename packages/pl-fe/src/features/notifications/components/{notification.test.tsx => notifications.test.tsx} (100%) delete mode 100644 packages/pl-fe/src/features/search/components/search.tsx delete mode 100644 packages/pl-fe/src/features/search/index.tsx rename packages/pl-fe/src/{features/account-gallery/components/media-item.tsx => pages/accounts/account-gallery.tsx} (58%) rename packages/pl-fe/src/{features/account-timeline/index.tsx => pages/accounts/account-timeline.tsx} (95%) rename packages/pl-fe/src/{features/circle/index.tsx => pages/fun/circle.tsx} (99%) rename packages/pl-fe/src/{features/notifications/index.tsx => pages/notifications/notifications.tsx} (67%) rename packages/pl-fe/src/{features/search/components/search-results.tsx => pages/search/search.tsx} (77%) create mode 100644 packages/pl-fe/src/pages/settings/filters.tsx rename packages/pl-fe/src/{features/bookmark-folders/index.tsx => pages/status-lists/bookmark-folders.tsx} (97%) rename packages/pl-fe/src/{features/compose-event/index.tsx => pages/statuses/compose-event.tsx} (84%) diff --git a/packages/pl-fe/src/features/account-gallery/index.tsx b/packages/pl-fe/src/features/account-gallery/index.tsx deleted file mode 100644 index 5f0db977a..000000000 --- a/packages/pl-fe/src/features/account-gallery/index.tsx +++ /dev/null @@ -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 ( - - - - ); - } - - if (!account) { - return ( - - ); - } - - let loadOlder = null; - - if (hasMore && !(isFetching && attachments.length === 0)) { - loadOlder = ; - } - - if (isUnavailable) { - return ( - -
- -
-
- ); - } - - return ( - -
- {attachments.map((attachment, index) => ( - - ))} - - {!isLoading && attachments.length === 0 && ( -
- -
- )} -
- - {loadOlder} - - {isFetching && attachments.length === 0 && ( -
- -
- )} -
- ); -}; - -export { AccountGallery as default }; diff --git a/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx b/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx deleted file mode 100644 index da8b93228..000000000 --- a/packages/pl-fe/src/features/account-timeline/components/moved-note.tsx +++ /dev/null @@ -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 = ({ from, to }) => ( -
- - - -
- - , - targetName: to.acct, - }} - /> - -
-
- - -
-); - -export { MovedNote as default }; diff --git a/packages/pl-fe/src/features/account/components/header.tsx b/packages/pl-fe/src/features/account/components/header.tsx index 67a2fa11a..6f5d2af4a 100644 --- a/packages/pl-fe/src/features/account/components/header.tsx +++ b/packages/pl-fe/src/features/account/components/header.tsx @@ -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 = ({ from, to }) => ( +
+ + + +
+ + , + targetName: to.acct, + }} + /> + +
+
+ + +
+); + interface IHeader { account?: Account; } diff --git a/packages/pl-fe/src/features/followed-tags/index.tsx b/packages/pl-fe/src/features/followed-tags/index.tsx deleted file mode 100644 index 1c0b79830..000000000 --- a/packages/pl-fe/src/features/followed-tags/index.tsx +++ /dev/null @@ -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 = ; - - return ( - - - {tags.map(tag => )} - - - ); -}; - -export { FollowedTags as default }; diff --git a/packages/pl-fe/src/features/notifications/components/filter-bar.tsx b/packages/pl-fe/src/features/notifications/components/filter-bar.tsx deleted file mode 100644 index fa97aab96..000000000 --- a/packages/pl-fe/src/features/notifications/components/filter-bar.tsx +++ /dev/null @@ -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: , - title: intl.formatMessage(messages.mentions), - action: onClick('mention'), - name: 'mention', - }); - if (features.accountNotifies) items.push({ - text: , - title: intl.formatMessage(messages.statuses), - action: onClick('status'), - name: 'status', - }); - items.push({ - text: , - title: intl.formatMessage(messages.favourites), - action: onClick('favourite'), - name: 'favourite', - }); - items.push({ - text: , - title: intl.formatMessage(messages.boosts), - action: onClick('reblog'), - name: 'reblog', - }); - if (features.polls) items.push({ - text: , - title: intl.formatMessage(messages.polls), - action: onClick('poll'), - name: 'poll', - }); - if (features.events) items.push({ - text: , - title: intl.formatMessage(messages.events), - action: onClick('events'), - name: 'events', - }); - items.push({ - text: , - title: intl.formatMessage(messages.follows), - action: onClick('follow'), - name: 'follow', - }); - } - - return ; -}; - -export { NotificationFilterBar as default }; diff --git a/packages/pl-fe/src/features/notifications/components/notification.test.tsx b/packages/pl-fe/src/features/notifications/components/notifications.test.tsx similarity index 100% rename from packages/pl-fe/src/features/notifications/components/notification.test.tsx rename to packages/pl-fe/src/features/notifications/components/notifications.test.tsx diff --git a/packages/pl-fe/src/features/search/components/search.tsx b/packages/pl-fe/src/features/search/components/search.tsx deleted file mode 100644 index 342c3c261..000000000 --- a/packages/pl-fe/src/features/search/components/search.tsx +++ /dev/null @@ -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) => { - const { value } = event.target; - - setValue(value); - }; - - const handleClick = (event: React.MouseEvent) => { - event.preventDefault(); - - if (params.get('q') === value) { - if (value.length > 0) { - setValue(''); - setQuery(''); - } - } else { - setQuery(value); - } - }; - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter') { - event.preventDefault(); - - setQuery(value); - } else if (event.key === 'Escape') { - document.querySelector('.ui')?.parentElement?.focus(); - } - }; - - return ( -
- - -
- - -
- {params.get('q') === value ? ( - - ) : ( - - )} - -
-
-
- ); -}; - -export { Search as default }; diff --git a/packages/pl-fe/src/features/search/index.tsx b/packages/pl-fe/src/features/search/index.tsx deleted file mode 100644 index 2aa74d089..000000000 --- a/packages/pl-fe/src/features/search/index.tsx +++ /dev/null @@ -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 ( - -
- - -
-
- ); -}; - -export { SearchPage as default }; 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 b817203a6..4c70e4c8d 100644 --- a/packages/pl-fe/src/features/ui/util/async-components.ts +++ b/packages/pl-fe/src/features/ui/util/async-components.ts @@ -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')); diff --git a/packages/pl-fe/src/features/account-gallery/components/media-item.tsx b/packages/pl-fe/src/pages/accounts/account-gallery.tsx similarity index 58% rename from packages/pl-fe/src/features/account-gallery/components/media-item.tsx rename to packages/pl-fe/src/pages/accounts/account-gallery.tsx index 159d70e2d..87be15b81 100644 --- a/packages/pl-fe/src/features/account-gallery/components/media-item.tsx +++ b/packages/pl-fe/src/pages/accounts/account-gallery.tsx @@ -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 = ({ 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 ( + + + + ); + } + + if (!account) { + return ( + + ); + } + + let loadOlder = null; + + if (hasMore && !(isFetching && attachments.length === 0)) { + loadOlder = ; + } + + if (isUnavailable) { + return ( + +
+ +
+
+ ); + } + + return ( + +
+ {attachments.map((attachment, index) => ( + + ))} + + {!isLoading && attachments.length === 0 && ( +
+ +
+ )} +
+ + {loadOlder} + + {isFetching && attachments.length === 0 && ( +
+ +
+ )} +
+ ); +}; + +export { AccountGalleryPage as default }; diff --git a/packages/pl-fe/src/features/account-timeline/index.tsx b/packages/pl-fe/src/pages/accounts/account-timeline.tsx similarity index 95% rename from packages/pl-fe/src/features/account-timeline/index.tsx rename to packages/pl-fe/src/pages/accounts/account-timeline.tsx index f46558aa9..5e0d1ff36 100644 --- a/packages/pl-fe/src/features/account-timeline/index.tsx +++ b/packages/pl-fe/src/pages/accounts/account-timeline.tsx @@ -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 = ({ params, withReplies = false }) => { +const AccountTimelinePage: React.FC = ({ params, withReplies = false }) => { const history = useHistory(); const dispatch = useAppDispatch(); const features = useFeatures(); @@ -103,4 +103,4 @@ const AccountTimeline: React.FC = ({ params, withReplies = fal ); }; -export { AccountTimeline as default }; +export { AccountTimelinePage as default }; diff --git a/packages/pl-fe/src/features/circle/index.tsx b/packages/pl-fe/src/pages/fun/circle.tsx similarity index 99% rename from packages/pl-fe/src/features/circle/index.tsx rename to packages/pl-fe/src/pages/fun/circle.tsx index db534a7be..e318d9853 100644 --- a/packages/pl-fe/src/features/circle/index.tsx +++ b/packages/pl-fe/src/pages/fun/circle.tsx @@ -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 }; diff --git a/packages/pl-fe/src/features/notifications/index.tsx b/packages/pl-fe/src/pages/notifications/notifications.tsx similarity index 67% rename from packages/pl-fe/src/features/notifications/index.tsx rename to packages/pl-fe/src/pages/notifications/notifications.tsx index 77ddbac2f..9d01a512e 100644 --- a/packages/pl-fe/src/features/notifications/index.tsx +++ b/packages/pl-fe/src/pages/notifications/notifications.tsx @@ -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: , + title: intl.formatMessage(messages.mentions), + action: onClick('mention'), + name: 'mention', + }); + if (features.accountNotifies) items.push({ + text: , + title: intl.formatMessage(messages.statuses), + action: onClick('status'), + name: 'status', + }); + items.push({ + text: , + title: intl.formatMessage(messages.favourites), + action: onClick('favourite'), + name: 'favourite', + }); + items.push({ + text: , + title: intl.formatMessage(messages.boosts), + action: onClick('reblog'), + name: 'reblog', + }); + if (features.polls) items.push({ + text: , + title: intl.formatMessage(messages.polls), + action: onClick('poll'), + name: 'poll', + }); + if (features.events) items.push({ + text: , + title: intl.formatMessage(messages.events), + action: onClick('events'), + name: 'events', + }); + items.push({ + text: , + title: intl.formatMessage(messages.follows), + action: onClick('follow'), + name: 'follow', + }); + } + + return ; +}; + 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 }; diff --git a/packages/pl-fe/src/features/search/components/search-results.tsx b/packages/pl-fe/src/pages/search/search.tsx similarity index 77% rename from packages/pl-fe/src/features/search/components/search-results.tsx rename to packages/pl-fe/src/pages/search/search.tsx index 4fae1ed0b..f73160cf0 100644 --- a/packages/pl-fe/src/features/search/components/search-results.tsx +++ b/packages/pl-fe/src/pages/search/search.tsx @@ -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) => { + const { value } = event.target; + + setValue(value); + }; + + const handleClick = (event: React.MouseEvent) => { + event.preventDefault(); + + if (params.get('q') === value) { + if (value.length > 0) { + setValue(''); + setQuery(''); + } + } else { + setQuery(value); + } + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + + setQuery(value); + } else if (event.key === 'Escape') { + document.querySelector('.ui')?.parentElement?.focus(); + } + }; + + return ( +
+ + +
+ + +
+ {params.get('q') === value ? ( + + ) : ( + + )} + +
+
+
+ ); +}; + const SearchResults = () => { const node = useRef(null); @@ -274,4 +362,17 @@ const SearchResults = () => { ); }; -export { SearchResults as default }; +const SearchPage = () => { + const intl = useIntl(); + + return ( + +
+ + +
+
+ ); +}; + +export { SearchPage as default }; diff --git a/packages/pl-fe/src/pages/settings/filters.tsx b/packages/pl-fe/src/pages/settings/filters.tsx new file mode 100644 index 000000000..19616457d --- /dev/null +++ b/packages/pl-fe/src/pages/settings/filters.tsx @@ -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 = ; + + return ( + + + + + + + {filters.map((filter) => ( +
+ + + + + {' '} + {filter.keywords.map(keyword => keyword.keyword).join(', ')} + + + + {' '} + {filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')} + + + + {filtersV2 ? ( + filter.filter_action === 'hide' ? + : + + ) : (filter.filter_action === 'hide' ? + : + )} + + {filter.expires_at && ( + + {new Date(filter.expires_at).getTime() <= Date.now() + ? + : } + + )} + + + + + + + +
+ ))} +
+
+ ); +}; + +export { FiltersPage as default }; diff --git a/packages/pl-fe/src/pages/settings/index.tsx b/packages/pl-fe/src/pages/settings/index.tsx index 19616457d..8c28e8542 100644 --- a/packages/pl-fe/src/pages/settings/index.tsx +++ b/packages/pl-fe/src/pages/settings/index.tsx @@ -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 = ; + const emptyMessage = ; return ( - - - - - + - {filters.map((filter) => ( -
- - - - - {' '} - {filter.keywords.map(keyword => keyword.keyword).join(', ')} - - - - {' '} - {filter.context.map(context => contexts[context] ? intl.formatMessage(contexts[context]) : context).join(', ')} - - - - {filtersV2 ? ( - filter.filter_action === 'hide' ? - : - - ) : (filter.filter_action === 'hide' ? - : - )} - - {filter.expires_at && ( - - {new Date(filter.expires_at).getTime() <= Date.now() - ? - : } - - )} - - - - - - - -
- ))} + {tags.map(tag => )}
); }; -export { FiltersPage as default }; +export { FollowedTagsPage as default }; diff --git a/packages/pl-fe/src/features/bookmark-folders/index.tsx b/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx similarity index 97% rename from packages/pl-fe/src/features/bookmark-folders/index.tsx rename to packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx index 3635d114e..18fc47608 100644 --- a/packages/pl-fe/src/features/bookmark-folders/index.tsx +++ b/packages/pl-fe/src/pages/status-lists/bookmark-folders.tsx @@ -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 }; diff --git a/packages/pl-fe/src/features/compose-event/index.tsx b/packages/pl-fe/src/pages/statuses/compose-event.tsx similarity index 84% rename from packages/pl-fe/src/features/compose-event/index.tsx rename to packages/pl-fe/src/pages/statuses/compose-event.tsx index 2e3d300d2..d9aa4b330 100644 --- a/packages/pl-fe/src/features/compose-event/index.tsx +++ b/packages/pl-fe/src/pages/statuses/compose-event.tsx @@ -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 = ({ params }) => { +const ComposeEventPage: React.FC = ({ params }) => { const intl = useIntl(); const dispatch = useAppDispatch(); @@ -64,4 +64,4 @@ const ComposeEvent: React.FC = ({ params }) => { ); }; -export { ComposeEvent as default }; +export { ComposeEventPage as default };