pl-fe: initial support for kmyblue antennas

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2026-01-23 19:53:18 +01:00
parent d6565fd7f8
commit 0dcb54b9a3
10 changed files with 376 additions and 2 deletions

View File

@ -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<string, any> = {}, expand = false, done = noOp) =>
async (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState();
@ -408,6 +424,7 @@ export {
fetchAccountTimeline,
fetchListTimeline,
fetchCircleTimeline,
fetchAntennaTimeline,
fetchGroupTimeline,
fetchHashtagTimeline,
fetchLinkTimeline,

View File

@ -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')),

View File

@ -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,

View File

@ -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'));

View File

@ -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.",

View File

@ -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<IAntennaMembersForm> = () => null;
interface IEditAntennaForm {
antennaId?: string;
setAntennaId: (id: string | undefined) => void;
onTabChange: (tab: Tab) => void;
}
const EditAntennaForm: React.FC<IEditAntennaForm> = ({ 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<Element> = 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 (
<Form onSubmit={handleSubmit}>
<FormGroup
labelText={<FormattedMessage id='antennas.edit.title' defaultMessage='Antenna title' />}
>
<Input
outerClassName='grow'
type='text'
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</FormGroup>
<FormActions>
<Button onClick={handleUpdate} disabled={disabled}>
{antennaId
? <FormattedMessage id='antennas.edit.save' defaultMessage='Save antenna' />
: <FormattedMessage id='antennas.create.save' defaultMessage='Create antenna' />}
</Button>
</FormActions>
</Form>
);
};
interface AntennaEditorModalProps {
antennaId?: string;
}
const AntennaEditorModal: React.FC<BaseModalProps & AntennaEditorModalProps> = ({ antennaId: initialAntennaId, onClose }) => {
const [antennaId, setAntennaId] = useState<string | undefined>(initialAntennaId);
const [tab, setTab] = useState<Tab>('info');
const { isFetched } = useAntenna(antennaId);
const onClickClose = () => {
onClose('ANTENNA_EDITOR');
};
return (
<Modal
title={antennaId
? <FormattedMessage id='antennas.edit' defaultMessage='Edit antenna' />
: <FormattedMessage id='antennas.create' defaultMessage='Create antenna' />}
onClose={onClickClose}
onBack={tab === 'info' ? undefined : () => setTab('info')}
>
{isFetched ? (tab === 'info'
? <EditAntennaForm antennaId={antennaId} setAntennaId={setAntennaId} onTabChange={setTab} />
: <AntennaMembersForm antennaId={antennaId} />
) : <Spinner />}
</Modal>
);
};
export { AntennaEditorModal as default, type AntennaEditorModalProps };

View File

@ -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' },

View File

@ -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 (
<Column>
<Spinner />
</Column>
);
}
const items = [
{
text: intl.formatMessage(messages.createAntenna),
action: () => openModal('ANTENNA_EDITOR', {}),
icon: require('@phosphor-icons/core/regular/plus.svg'),
},
];
const emptyMessage = <FormattedMessage id='empty_column.antennas' defaultMessage="You don't have any antennas yet. When you create one, it will show up here." />;
return (
<Column
label={intl.formatMessage(messages.heading)}
action={<DropdownMenu items={items} src={require('@phosphor-icons/core/regular/dots-three-vertical.svg')} />}
>
<Stack space={4}>
{/* <NewListForm /> */}
{!Object.keys(antennas).length ? (
<Card variant='rounded' size='lg'>
{emptyMessage}
</Card>
) : (
<List>
{antennas.map((antenna: any) => (
<ListItem
key={antenna.id}
to='/antennas/$antennaId'
params={{ antennaId: antenna.id }}
label={
<HStack alignItems='center' space={2}>
<Icon src={require('@phosphor-icons/core/regular/list-bullets.svg')} size={20} />
<span>{antenna.title}</span>
</HStack>
}
/>
))}
</List>
)}
</Stack>
</Column>
);
};
export { AntennasPage as default };

View File

@ -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 (
<Column>
<div>
<Spinner />
</div>
</Column>
);
} else if (!antenna) {
return (
<MissingIndicator />
);
}
const emptyMessage = (
<div>
<FormattedMessage id='empty_column.antenna' defaultMessage='There is nothing in this antenna yet. When posts matching the criteria will be created, they will appear here.' />
{/* <br /><br />
<Button onClick={handleEditClick}><FormattedMessage id='circle.click_to_add' defaultMessage='Click here to add people' /></Button> */}
</div>
);
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 (
<Column
label={title}
action={<DropdownMenu items={items} src={require('@phosphor-icons/core/regular/dots-three-vertical.svg')} />}
>
<Timeline
loadMoreClassName='sm:pb-4 black:sm:pb-0 black:sm:mx-4'
scrollKey='antenna_timeline'
timelineId={`antenna:${antennaId}`}
onLoadMore={handleLoadMore}
emptyMessageText={emptyMessage}
emptyMessageIcon={require('@phosphor-icons/core/regular/chat-centered-text.svg')}
/>
</Column>
);
};
export { AntennaTimelinePage as default };

View File

@ -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]