From 0dcb54b9a35dcded92eb380546a1da7a93c19934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Fri, 23 Jan 2026 19:53:18 +0100 Subject: [PATCH] pl-fe: initial support for kmyblue antennas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/timelines.ts | 17 +++ .../src/features/ui/components/modal-root.tsx | 1 + .../pl-fe/src/features/ui/router/index.tsx | 22 ++++ .../src/features/ui/util/async-components.ts | 2 + packages/pl-fe/src/locales/en.json | 17 ++- .../pl-fe/src/modals/antenna-editor-modal.tsx | 115 +++++++++++++++++ .../components/edit-list-form.tsx | 1 - .../src/pages/account-lists/antennas.tsx | 82 ++++++++++++ .../src/pages/timelines/antenna-timeline.tsx | 119 ++++++++++++++++++ packages/pl-fe/src/stores/modals.ts | 2 + 10 files changed, 376 insertions(+), 2 deletions(-) create mode 100644 packages/pl-fe/src/modals/antenna-editor-modal.tsx create mode 100644 packages/pl-fe/src/pages/account-lists/antennas.tsx create mode 100644 packages/pl-fe/src/pages/timelines/antenna-timeline.tsx diff --git a/packages/pl-fe/src/actions/timelines.ts b/packages/pl-fe/src/actions/timelines.ts index 5d8ec937c..89b1b6dc0 100644 --- a/packages/pl-fe/src/actions/timelines.ts +++ b/packages/pl-fe/src/actions/timelines.ts @@ -8,6 +8,7 @@ import { importEntities } from './importer'; import type { Account as BaseAccount, + AntennaTimelineParams, GetAccountStatusesParams, GetCircleStatusesParams, GroupTimelineParams, @@ -290,6 +291,21 @@ const fetchCircleTimeline = (circleId: string, expand = false, done = noOp) => return dispatch(handleTimelineExpand(timelineId, fn, done)); }; +const fetchAntennaTimeline = (antennaId: string, expand = false, done = noOp) => + async (dispatch: AppDispatch, getState: () => RootState) => { + const state = getState(); + const timelineId = `antenna:${antennaId}`; + + const params: AntennaTimelineParams = {}; + // if (useSettingsStore.getState().settings.autoTranslate) params.language = getLocale(); + + if (expand && state.timelines[timelineId]?.isLoading) return; + + const fn = (expand && state.timelines[timelineId]?.next?.()) || getClient(state).timelines.antennaTimeline(antennaId, params); + + return dispatch(handleTimelineExpand(timelineId, fn, done)); + }; + const fetchGroupTimeline = (groupId: string, { only_media, limit }: Record = {}, expand = false, done = noOp) => async (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -408,6 +424,7 @@ export { fetchAccountTimeline, fetchListTimeline, fetchCircleTimeline, + fetchAntennaTimeline, fetchGroupTimeline, fetchHashtagTimeline, fetchLinkTimeline, diff --git a/packages/pl-fe/src/features/ui/components/modal-root.tsx b/packages/pl-fe/src/features/ui/components/modal-root.tsx index 6fa0cd3c8..96189e95d 100644 --- a/packages/pl-fe/src/features/ui/components/modal-root.tsx +++ b/packages/pl-fe/src/features/ui/components/modal-root.tsx @@ -10,6 +10,7 @@ import ModalLoading from './modal-loading'; /* eslint sort-keys: "error" */ const MODAL_COMPONENTS = { ALT_TEXT: lazy(() => import('pl-fe/modals/alt-text-modal')), + ANTENNA_EDITOR: lazy(() => import('pl-fe/modals/antenna-editor-modal')), BIRTHDAYS: lazy(() => import('pl-fe/modals/birthdays-modal')), BLOCK_MUTE: lazy(() => import('pl-fe/modals/block-mute-modal')), BOOST: lazy(() => import('pl-fe/modals/boost-modal')), diff --git a/packages/pl-fe/src/features/ui/router/index.tsx b/packages/pl-fe/src/features/ui/router/index.tsx index 79a34e1c9..72d278ae0 100644 --- a/packages/pl-fe/src/features/ui/router/index.tsx +++ b/packages/pl-fe/src/features/ui/router/index.tsx @@ -51,6 +51,8 @@ import { AdminAccount, Aliases, Announcements, + Antennas, + AntennaTimeline, AuthTokenList, Backups, Blocks, @@ -464,6 +466,24 @@ export const circleTimelineRoute = createRoute({ }), }); +export const antennasRoute = createRoute({ + getParentRoute: () => layouts.default, + path: '/antennas', + component: Antennas, + beforeLoad: requireAuthMiddleware(({ context: { features } }) => { + if (!features.antennas) throw notFound(); + }), +}); + +export const antennaTimelineRoute = createRoute({ + getParentRoute: () => layouts.default, + path: '/antennas/$antennaId', + component: AntennaTimeline, + beforeLoad: requireAuthMiddleware(({ context: { features } }) => { + if (!features.antennas) throw notFound(); + }), +}); + // Bookmarks export const bookmarkFoldersRoute = createRoute({ getParentRoute: () => layouts.default, @@ -1301,6 +1321,8 @@ const routeTree = rootRoute.addChildren([ listTimelineRoute, circlesRoute, circleTimelineRoute, + antennasRoute, + antennaTimelineRoute, bookmarkFoldersRoute, bookmarksRoute, notificationsRoute, 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 5dbd24ced..1ebe8a8a0 100644 --- a/packages/pl-fe/src/features/ui/util/async-components.ts +++ b/packages/pl-fe/src/features/ui/util/async-components.ts @@ -7,6 +7,8 @@ export const AccountTimeline = lazy(() => import('pl-fe/pages/accounts/account-t export const AdminAccount = lazy(() => import('pl-fe/pages/dashboard/account')); export const Aliases = lazy(() => import('pl-fe/pages/settings/aliases')); export const Announcements = lazy(() => import('pl-fe/pages/dashboard/announcements')); +export const Antennas = lazy(() => import('pl-fe/pages/account-lists/antennas')); +export const AntennaTimeline = lazy(() => import('pl-fe/pages/timelines/antenna-timeline')); export const AuthTokenList = lazy(() => import('pl-fe/pages/settings/auth-token-list')); export const AwaitingApproval = lazy(() => import('pl-fe/pages/dashboard/awaiting-approval')); export const Backups = lazy(() => import('pl-fe/pages/settings/backups')); diff --git a/packages/pl-fe/src/locales/en.json b/packages/pl-fe/src/locales/en.json index cecdaac15..24ddbb784 100644 --- a/packages/pl-fe/src/locales/en.json +++ b/packages/pl-fe/src/locales/en.json @@ -256,6 +256,16 @@ "alt_text_modal.header": "Add alt text", "alt_text_modal.saving_failed": "Failed to save alt text", "announcements.title": "Announcements", + "antennas.create": "Create antenna", + "antennas.create.save": "Create antenna", + "antennas.delete": "Delete antenna", + "antennas.edit": "Edit antenna", + "antennas.edit.error": "Error updating antenna", + "antennas.edit.save": "Save antenna", + "antennas.edit.success": "Antenna updated successfully", + "antennas.edit.title": "Antenna title", + "antennas.new.create": "Add antenna", + "antennas.subheading": "Your antennas", "app_create.name_label": "App name", "app_create.name_placeholder": "e.g. 'pl-fe'", "app_create.redirect_uri_label": "Redirect URIs", @@ -389,6 +399,7 @@ "column.aliases.delete_error": "Error deleting alias", "column.aliases.subheading_add_new": "Add new alias", "column.aliases.subheading_aliases": "Current aliases", + "column.antennas": "Antennas", "column.app_create": "Create app", "column.backups": "Backups", "column.birthdays": "Birthdays", @@ -634,6 +645,9 @@ "confirmations.delete.confirm": "Delete", "confirmations.delete.heading": "Delete post", "confirmations.delete.message": "Are you sure you want to delete this post?", + "confirmations.delete_antenna.confirm": "Delete", + "confirmations.delete_antenna.heading": "Delete antenna", + "confirmations.delete_antenna.message": "Are you sure you want to permanently delete this antenna?", "confirmations.delete_bookmark_folder.confirm": "Delete folder", "confirmations.delete_bookmark_folder.heading": "Delete \"{name}\" folder?", "confirmations.delete_bookmark_folder.message": "Are you sure you want to delete the folder? The bookmarks will still be stored.", @@ -864,6 +878,8 @@ "empty_column.admin.rules": "There are no instance rules yet.", "empty_column.aliases": "You haven't created any account alias yet.", "empty_column.aliases.suggestions": "There are no account suggestions available for the provided term.", + "empty_column.antenna": "There is nothing in this antenna yet. When posts matching the criteria will be created, they will appear here.", + "empty_column.antennas": "You don't have any antennas yet. When you create one, it will show up here.", "empty_column.blocks": "You haven't blocked any users yet.", "empty_column.bookmarks": "You don't have any bookmarks yet. When you add one, it will show up here.", "empty_column.bookmarks.folder": "You don't have any bookmarks in this folder yet. When you add one, it will show up here.", @@ -1189,7 +1205,6 @@ "lists.manage_members": "Manage list members", "lists.new.create": "Add list", "lists.new.create_title": "Add list", - "lists.new.save": "Save list", "lists.new.title_placeholder": "New list title", "lists.notifications": "Subscribe", "lists.notifications_hint": "Receive notifications for new posts in the list.", diff --git a/packages/pl-fe/src/modals/antenna-editor-modal.tsx b/packages/pl-fe/src/modals/antenna-editor-modal.tsx new file mode 100644 index 000000000..3d5bb8eae --- /dev/null +++ b/packages/pl-fe/src/modals/antenna-editor-modal.tsx @@ -0,0 +1,115 @@ +import React, { useState } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import Button from 'pl-fe/components/ui/button'; +import Form from 'pl-fe/components/ui/form'; +import FormActions from 'pl-fe/components/ui/form-actions'; +import FormGroup from 'pl-fe/components/ui/form-group'; +import Input from 'pl-fe/components/ui/input'; +import Modal from 'pl-fe/components/ui/modal'; +import Spinner from 'pl-fe/components/ui/spinner'; +import { useAntenna, useCreateAntenna, useUpdateAntenna } from 'pl-fe/queries/accounts/use-antennas'; +import toast from 'pl-fe/toast'; + +import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root'; + +type Tab = 'info' | 'accounts' | 'excludedAccounts'; + +const messages = defineMessages({ + success: { id: 'antennas.edit.success', defaultMessage: 'Antenna updated successfully' }, + error: { id: 'antennas.edit.error', defaultMessage: 'Error updating antenna' }, +}); + +interface IAntennaMembersForm { + antennaId?: string; +} + +const AntennaMembersForm: React.FC = () => null; + +interface IEditAntennaForm { + antennaId?: string; + setAntennaId: (id: string | undefined) => void; + onTabChange: (tab: Tab) => void; +} + +const EditAntennaForm: React.FC = ({ antennaId, onTabChange }) => { + const intl = useIntl(); + + const { data: antenna } = useAntenna(antennaId); + const { mutate: updateAntenna, isPending: updateDisabled } = useUpdateAntenna(antennaId!); + const { mutate: createAntenna, isPending: createDisabled } = useCreateAntenna(); + + const disabled = antennaId ? updateDisabled : createDisabled; + + const [title, setTitle] = useState(antenna ? antenna.title : ''); + + const handleSubmit: React.FormEventHandler = e => { + e.preventDefault(); + handleUpdate(); + }; + + const handleUpdate = () => { + (antennaId ? updateAntenna : createAntenna)({ title }, { + onSuccess: () => { + toast.success(intl.formatMessage(messages.success)); + }, + onError: () => { + toast.error(intl.formatMessage(messages.error)); + }, + }); + }; + + return ( +
+ } + > + setTitle(e.target.value)} + /> + + + + +
+ ); +}; + +interface AntennaEditorModalProps { + antennaId?: string; +} + +const AntennaEditorModal: React.FC = ({ antennaId: initialAntennaId, onClose }) => { + const [antennaId, setAntennaId] = useState(initialAntennaId); + const [tab, setTab] = useState('info'); + + const { isFetched } = useAntenna(antennaId); + + const onClickClose = () => { + onClose('ANTENNA_EDITOR'); + }; + + return ( + + : } + onClose={onClickClose} + onBack={tab === 'info' ? undefined : () => setTab('info')} + > + {isFetched ? (tab === 'info' + ? + : + ) : } + + ); +}; + +export { AntennaEditorModal as default, type AntennaEditorModalProps }; diff --git a/packages/pl-fe/src/modals/list-editor-modal/components/edit-list-form.tsx b/packages/pl-fe/src/modals/list-editor-modal/components/edit-list-form.tsx index 27b044b82..63eb26b6c 100644 --- a/packages/pl-fe/src/modals/list-editor-modal/components/edit-list-form.tsx +++ b/packages/pl-fe/src/modals/list-editor-modal/components/edit-list-form.tsx @@ -14,7 +14,6 @@ import { useList, useUpdateList } from 'pl-fe/queries/accounts/use-lists'; import toast from 'pl-fe/toast'; const messages = defineMessages({ - save: { id: 'lists.new.save', defaultMessage: 'Save list' }, repliesPolicyNone: { id: 'lists.replies_policy.none', defaultMessage: 'No one' }, repliesPolicyList: { id: 'lists.replies_policy.list', defaultMessage: 'Members of the list' }, repliesPolicyFollowed: { id: 'lists.replies_policy.followed', defaultMessage: 'Any followed user' }, diff --git a/packages/pl-fe/src/pages/account-lists/antennas.tsx b/packages/pl-fe/src/pages/account-lists/antennas.tsx new file mode 100644 index 000000000..4fcea74e1 --- /dev/null +++ b/packages/pl-fe/src/pages/account-lists/antennas.tsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import DropdownMenu from 'pl-fe/components/dropdown-menu'; +import List, { ListItem } from 'pl-fe/components/list'; +import Card from 'pl-fe/components/ui/card'; +import Column from 'pl-fe/components/ui/column'; +import HStack from 'pl-fe/components/ui/hstack'; +import Icon from 'pl-fe/components/ui/icon'; +import Spinner from 'pl-fe/components/ui/spinner'; +import Stack from 'pl-fe/components/ui/stack'; +import { useAntennas } from 'pl-fe/queries/accounts/use-antennas'; +import { useModalsActions } from 'pl-fe/stores/modals'; + +import { getOrderedLists } from './lists'; + +const messages = defineMessages({ + heading: { id: 'column.antennas', defaultMessage: 'Antennas' }, + subheading: { id: 'antennas.subheading', defaultMessage: 'Your antennas' }, + createAntenna: { id: 'antennas.new.create', defaultMessage: 'Add antenna' }, +}); + +const AntennasPage: React.FC = () => { + const intl = useIntl(); + + const { openModal } = useModalsActions(); + + const { data: antennas } = useAntennas(getOrderedLists); + + if (!antennas) { + return ( + + + + ); + } + + const items = [ + { + text: intl.formatMessage(messages.createAntenna), + action: () => openModal('ANTENNA_EDITOR', {}), + icon: require('@phosphor-icons/core/regular/plus.svg'), + }, + ]; + + const emptyMessage = ; + + return ( + } + > + + {/* */} + + {!Object.keys(antennas).length ? ( + + {emptyMessage} + + ) : ( + + {antennas.map((antenna: any) => ( + + + {antenna.title} + + } + /> + ))} + + )} + + + ); +}; + +export { AntennasPage as default }; diff --git a/packages/pl-fe/src/pages/timelines/antenna-timeline.tsx b/packages/pl-fe/src/pages/timelines/antenna-timeline.tsx new file mode 100644 index 000000000..e53bb276b --- /dev/null +++ b/packages/pl-fe/src/pages/timelines/antenna-timeline.tsx @@ -0,0 +1,119 @@ +import { useNavigate } from '@tanstack/react-router'; +import React, { useEffect } from 'react'; +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; + +import { fetchAntennaTimeline } from 'pl-fe/actions/timelines'; +import DropdownMenu from 'pl-fe/components/dropdown-menu'; +import MissingIndicator from 'pl-fe/components/missing-indicator'; +// import Button from 'pl-fe/components/ui/button'; +import Column from 'pl-fe/components/ui/column'; +import Spinner from 'pl-fe/components/ui/spinner'; +import Timeline from 'pl-fe/features/ui/components/timeline'; +import { antennaTimelineRoute } from 'pl-fe/features/ui/router'; +import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useAntenna, useDeleteAntenna } from 'pl-fe/queries/accounts/use-antennas'; +import { useModalsActions } from 'pl-fe/stores/modals'; + +const messages = defineMessages({ + deleteHeading: { id: 'confirmations.delete_antenna.heading', defaultMessage: 'Delete antenna' }, + deleteMessage: { id: 'confirmations.delete_antenna.message', defaultMessage: 'Are you sure you want to permanently delete this antenna?' }, + deleteConfirm: { id: 'confirmations.delete_antenna.confirm', defaultMessage: 'Delete' }, + editAntenna: { id: 'antennas.edit', defaultMessage: 'Edit antenna' }, + deleteAntenna: { id: 'antennas.delete', defaultMessage: 'Delete antenna' }, +}); + +const AntennaTimelinePage: React.FC = () => { + const { antennaId } = antennaTimelineRoute.useParams(); + + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { openModal } = useModalsActions(); + const navigate = useNavigate(); + + const { data: antenna, isFetching } = useAntenna(antennaId); + const { mutate: deleteAntenna } = useDeleteAntenna(); + + useEffect(() => { + dispatch(fetchAntennaTimeline(antennaId)); + }, [antennaId]); + + const handleLoadMore = () => { + dispatch(fetchAntennaTimeline(antennaId, true)); + }; + + const handleEditClick = () => { + openModal('ANTENNA_EDITOR', { antennaId }); + }; + + const handleDeleteClick = (e: React.MouseEvent | React.KeyboardEvent) => { + e.preventDefault(); + + openModal('CONFIRM', { + heading: intl.formatMessage(messages.deleteHeading), + message: intl.formatMessage(messages.deleteMessage), + confirm: intl.formatMessage(messages.deleteConfirm), + onConfirm: () => { + deleteAntenna(antennaId, { + onSuccess: () => { + navigate({ to: '/antennas', replace: true }); + }, + }); + }, + }); + }; + + const title = antenna ? antenna.title : antennaId; + + if (!antenna && isFetching) { + return ( + +
+ +
+
+ ); + } else if (!antenna) { + return ( + + ); + } + + const emptyMessage = ( +
+ + {/*

+ */} +
+ ); + + const items = [ + { + text: intl.formatMessage(messages.editAntenna), + action: handleEditClick, + icon: require('@phosphor-icons/core/regular/pencil-simple.svg'), + }, + { + text: intl.formatMessage(messages.deleteAntenna), + action: handleDeleteClick, + icon: require('@phosphor-icons/core/regular/trash.svg'), + }, + ]; + + return ( + } + > + + + ); +}; + +export { AntennaTimelinePage as default }; diff --git a/packages/pl-fe/src/stores/modals.ts b/packages/pl-fe/src/stores/modals.ts index 822552841..ada812862 100644 --- a/packages/pl-fe/src/stores/modals.ts +++ b/packages/pl-fe/src/stores/modals.ts @@ -4,6 +4,7 @@ import { mutative } from 'zustand-mutative'; import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; import type { AltTextModalProps } from 'pl-fe/modals/alt-text-modal'; +import type { AntennaEditorModalProps } from 'pl-fe/modals/antenna-editor-modal'; import type { BlockMuteModalProps } from 'pl-fe/modals/block-mute-modal'; import type { BoostModalProps } from 'pl-fe/modals/boost-modal'; import type { CircleEditorModalProps } from 'pl-fe/modals/circle-editor-modal'; @@ -41,6 +42,7 @@ import type { UnauthorizedModalProps } from 'pl-fe/modals/unauthorized-modal'; type OpenModalProps = | [type: 'ALT_TEXT', props: AltTextModalProps] + | [type: 'ANTENNA_EDITOR', props: AntennaEditorModalProps] | [type: 'BIRTHDAYS' | 'CREATE_GROUP' | 'HOTKEYS'] | [type: 'BOOST', props: BoostModalProps] | [type: 'CIRCLE_EDITOR', props: CircleEditorModalProps]