pl-fe: move more files around, circle generation improvement?

Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
Nicole Mikołajczyk
2025-05-15 21:16:11 +02:00
parent 7f58121d69
commit f29850623f
11 changed files with 190 additions and 193 deletions

View File

@ -115,25 +115,29 @@ const Circle: React.FC = () => {
const avatarUrl = users[i].avatar || avatarMissing;
await new Promise(resolve => {
const img = new Image();
try {
await new Promise(resolve => {
const img = new Image();
img.onload = () => {
ctx.save();
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.clip();
img.onload = () => {
ctx.save();
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2);
ctx.restore();
ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2);
ctx.restore();
resolve(null);
};
resolve(null);
};
img.setAttribute('crossorigin', 'anonymous');
img.src = avatarUrl;
});
img.setAttribute('crossorigin', 'anonymous');
img.src = avatarUrl;
});
} catch (_) {
//
}
}
}

View File

@ -1,82 +0,0 @@
import React, { useCallback, useState } from 'react';
import { Link } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import EventPreview from 'pl-fe/components/event-preview';
import Card from 'pl-fe/components/ui/card';
import Icon from 'pl-fe/components/ui/icon';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import PlaceholderEventPreview from '../../placeholder/components/placeholder-event-preview';
const Event = ({ id }: { id: string }) => {
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id }));
if (!status) return null;
return (
<Link
className='w-full px-1'
to={`/@${status.account.acct}/events/${status.id}`}
>
<EventPreview status={status} floatingAction={false} />
</Link>
);
};
interface IEventCarousel {
statusIds: Array<string>;
isLoading?: boolean | null;
emptyMessage: React.ReactNode;
}
const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMessage }) => {
const [index, setIndex] = useState(0);
const handleChangeIndex = (index: number) => {
setIndex(index % statusIds.length);
};
if (statusIds.length === 0) {
if (isLoading) {
return <PlaceholderEventPreview />;
}
return (
<Card variant='rounded' size='lg'>
{emptyMessage}
</Card>
);
}
return (
<div className='relative -mx-1'>
{index !== 0 && (
<div className='absolute left-3 top-1/2 z-10 -mt-4'>
<button
onClick={() => handleChangeIndex(index - 1)}
className='flex size-8 items-center justify-center rounded-full bg-white/50 backdrop-blur dark:bg-gray-900/50'
>
<Icon src={require('@tabler/icons/outline/chevron-left.svg')} className='size-6 text-black dark:text-white' />
</button>
</div>
)}
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{statusIds.map(statusId => <Event key={statusId} id={statusId} />)}
</ReactSwipeableViews>
{index !== statusIds.length - 1 && (
<div className='absolute right-3 top-1/2 z-10 -mt-4'>
<button
onClick={() => handleChangeIndex(index + 1)}
className='flex size-8 items-center justify-center rounded-full bg-white/50 backdrop-blur dark:bg-gray-900/50'
>
<Icon src={require('@tabler/icons/outline/chevron-right.svg')} className='size-6 text-black dark:text-white' />
</button>
</div>
)}
</div>
);
};
export { EventCarousel as default };

View File

@ -1,62 +0,0 @@
import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { fetchJoinedEvents, fetchRecentEvents } from 'pl-fe/actions/events';
import Button from 'pl-fe/components/ui/button';
import { CardBody, CardHeader, CardTitle } from 'pl-fe/components/ui/card';
import Column from 'pl-fe/components/ui/column';
import HStack from 'pl-fe/components/ui/hstack';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import EventCarousel from './components/event-carousel';
const messages = defineMessages({
title: { id: 'column.events', defaultMessage: 'Events' },
});
const Events = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const recentEvents = useAppSelector((state) => state.status_lists.recent_events!.items);
const recentEventsLoading = useAppSelector((state) => state.status_lists.recent_events!.isLoading);
const joinedEvents = useAppSelector((state) => state.status_lists.joined_events!.items);
const joinedEventsLoading = useAppSelector((state) => state.status_lists.joined_events!.isLoading);
useEffect(() => {
dispatch(fetchRecentEvents());
dispatch(fetchJoinedEvents());
}, []);
return (
<Column label={intl.formatMessage(messages.title)}>
<HStack className='mb-2' space={2} justifyContent='between'>
<CardTitle title={<FormattedMessage id='events.recent_events' defaultMessage='Recent events' />} />
<Button className='ml-auto xl:hidden' theme='primary' size='sm' to='/events/new'>
<FormattedMessage id='events.create_event' defaultMessage='Create event' />
</Button>
</HStack>
<CardBody className='mb-2'>
<EventCarousel
statusIds={recentEvents}
isLoading={recentEventsLoading}
emptyMessage={<FormattedMessage id='events.recent_events.empty' defaultMessage='There are no public events yet.' />}
/>
</CardBody>
<CardHeader className='mb-2'>
<CardTitle title={<FormattedMessage id='events.joined_events' defaultMessage='Joined events' />} />
</CardHeader>
<CardBody>
<EventCarousel
statusIds={joinedEvents}
isLoading={joinedEventsLoading}
emptyMessage={<FormattedMessage id='events.joined_events.empty' defaultMessage="You haven't joined any event yet." />}
/>
</CardBody>
</Column>
);
};
export { Events as default };

View File

@ -10,6 +10,7 @@ export const Blocks = lazy(() => import('pl-fe/pages/settings/blocks'));
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 Circles = lazy(() => import('pl-fe/pages/account-lists/circles'));
export const CommunityTimeline = lazy(() => import('pl-fe/pages/timelines/community-timeline'));
export const Conversations = lazy(() => import('pl-fe/pages/status-lists/conversations'));
export const CreateApp = lazy(() => import('pl-fe/pages/developers/create-app'));
@ -17,6 +18,7 @@ export const CryptoDonate = lazy(() => import('pl-fe/pages/utils/crypto-donate')
export const Dashboard = lazy(() => import('pl-fe/pages/dashboard/dashboard'));
export const DeleteAccount = lazy(() => import('pl-fe/pages/settings/delete-account'));
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 EditEmail = lazy(() => import('pl-fe/pages/settings/edit-email'));
@ -29,6 +31,10 @@ 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 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'));
export const FollowRequests = lazy(() => import('pl-fe/pages/account-lists/follow-requests'));
export const GenericNotFound = lazy(() => import('pl-fe/pages/utils/generic-not-found'));
export const GroupBlockedMembers = lazy(() => import('pl-fe/pages/groups/group-blocked-members'));
export const GroupGallery = lazy(() => import('pl-fe/pages/groups/group-gallery'));
@ -44,6 +50,7 @@ export const InteractionPolicies = lazy(() => import('pl-fe/pages/settings/inter
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'));
export const Lists = lazy(() => import('pl-fe/pages/account-lists/lists'));
export const ListTimeline = lazy(() => import('pl-fe/pages/timelines/list-timeline'));
export const LoginPage = lazy(() => import('pl-fe/pages/auth/login'));
export const LogoutPage = lazy(() => import('pl-fe/pages/auth/logout'));
@ -53,6 +60,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 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'));
export const PlFeConfig = lazy(() => import('pl-fe/pages/dashboard/pl-fe-config'));
@ -73,30 +81,23 @@ 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 Circles = lazy(() => import('pl-fe/pages/account-lists/circles'));
export const ComposeEditor = lazy(() => import('pl-fe/features/compose/editor'));
export const ComposeEvent = lazy(() => import('pl-fe/features/compose-event'));
export const Directory = lazy(() => import('pl-fe/pages/account-lists/directory'));
export const DraftStatuses = lazy(() => import('pl-fe/features/draft-statuses'));
export const EventDiscussion = lazy(() => import('pl-fe/features/event/event-discussion'));
export const EventInformation = lazy(() => import('pl-fe/features/event/event-information'));
export const Events = lazy(() => import('pl-fe/features/events'));
export const FollowedTags = lazy(() => import('pl-fe/features/followed-tags'));
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'));
export const FollowRequests = lazy(() => import('pl-fe/pages/account-lists/follow-requests'));
export const InteractionRequests = lazy(() => import('pl-fe/features/interaction-requests'));
export const Lists = lazy(() => import('pl-fe/pages/account-lists/lists'));
export const Notifications = lazy(() => import('pl-fe/features/notifications'));
export const OutgoingFollowRequests = lazy(() => import('pl-fe/pages/account-lists/outgoing-follow-requests'));
export const ScheduledStatuses = lazy(() => import('pl-fe/features/scheduled-statuses'));
export const Search = lazy(() => import('pl-fe/features/search'));
export const Status = lazy(() => import('pl-fe/features/status'));
// Panels
export const AccountNotePanel = lazy(() => import('pl-fe/features/ui/components/panels/account-note-panel'));

View File

@ -7,13 +7,13 @@ import Column from 'pl-fe/components/ui/column';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import DraftStatus from './components/draft-status';
import DraftStatus from '../../features/draft-statuses/components/draft-status';
const messages = defineMessages({
heading: { id: 'column.draft_statuses', defaultMessage: 'Drafts' },
});
const DraftStatuses = () => {
const DraftStatusesPage = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -38,4 +38,4 @@ const DraftStatuses = () => {
);
};
export { DraftStatuses as default };
export { DraftStatusesPage as default };

View File

@ -0,0 +1,136 @@
import React, { useCallback, useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import ReactSwipeableViews from 'react-swipeable-views';
import { fetchJoinedEvents, fetchRecentEvents } from 'pl-fe/actions/events';
import EventPreview from 'pl-fe/components/event-preview';
import Button from 'pl-fe/components/ui/button';
import Card, { CardBody, CardHeader, CardTitle } 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 { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import PlaceholderEventPreview from '../../features/placeholder/components/placeholder-event-preview';
const messages = defineMessages({
title: { id: 'column.events', defaultMessage: 'Events' },
});
const Event = ({ id }: { id: string }) => {
const getStatus = useCallback(makeGetStatus(), []);
const status = useAppSelector(state => getStatus(state, { id }));
if (!status) return null;
return (
<Link
className='w-full px-1'
to={`/@${status.account.acct}/events/${status.id}`}
>
<EventPreview status={status} floatingAction={false} />
</Link>
);
};
interface IEventCarousel {
statusIds: Array<string>;
isLoading?: boolean | null;
emptyMessage: React.ReactNode;
}
const EventCarousel: React.FC<IEventCarousel> = ({ statusIds, isLoading, emptyMessage }) => {
const [index, setIndex] = useState(0);
const handleChangeIndex = (index: number) => {
setIndex(index % statusIds.length);
};
if (statusIds.length === 0) {
if (isLoading) {
return <PlaceholderEventPreview />;
}
return (
<Card variant='rounded' size='lg'>
{emptyMessage}
</Card>
);
}
return (
<div className='relative -mx-1'>
{index !== 0 && (
<div className='absolute left-3 top-1/2 z-10 -mt-4'>
<button
onClick={() => handleChangeIndex(index - 1)}
className='flex size-8 items-center justify-center rounded-full bg-white/50 backdrop-blur dark:bg-gray-900/50'
>
<Icon src={require('@tabler/icons/outline/chevron-left.svg')} className='size-6 text-black dark:text-white' />
</button>
</div>
)}
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{statusIds.map(statusId => <Event key={statusId} id={statusId} />)}
</ReactSwipeableViews>
{index !== statusIds.length - 1 && (
<div className='absolute right-3 top-1/2 z-10 -mt-4'>
<button
onClick={() => handleChangeIndex(index + 1)}
className='flex size-8 items-center justify-center rounded-full bg-white/50 backdrop-blur dark:bg-gray-900/50'
>
<Icon src={require('@tabler/icons/outline/chevron-right.svg')} className='size-6 text-black dark:text-white' />
</button>
</div>
)}
</div>
);
};
const EventsPage = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const recentEvents = useAppSelector((state) => state.status_lists.recent_events!.items);
const recentEventsLoading = useAppSelector((state) => state.status_lists.recent_events!.isLoading);
const joinedEvents = useAppSelector((state) => state.status_lists.joined_events!.items);
const joinedEventsLoading = useAppSelector((state) => state.status_lists.joined_events!.isLoading);
useEffect(() => {
dispatch(fetchRecentEvents());
dispatch(fetchJoinedEvents());
}, []);
return (
<Column label={intl.formatMessage(messages.title)}>
<HStack className='mb-2' space={2} justifyContent='between'>
<CardTitle title={<FormattedMessage id='events.recent_events' defaultMessage='Recent events' />} />
<Button className='ml-auto xl:hidden' theme='primary' size='sm' to='/events/new'>
<FormattedMessage id='events.create_event' defaultMessage='Create event' />
</Button>
</HStack>
<CardBody className='mb-2'>
<EventCarousel
statusIds={recentEvents}
isLoading={recentEventsLoading}
emptyMessage={<FormattedMessage id='events.recent_events.empty' defaultMessage='There are no public events yet.' />}
/>
</CardBody>
<CardHeader className='mb-2'>
<CardTitle title={<FormattedMessage id='events.joined_events' defaultMessage='Joined events' />} />
</CardHeader>
<CardBody>
<EventCarousel
statusIds={joinedEvents}
isLoading={joinedEventsLoading}
emptyMessage={<FormattedMessage id='events.joined_events.empty' defaultMessage="You haven't joined any event yet." />}
/>
</CardBody>
</Column>
);
};
export { EventsPage as default };

View File

@ -216,7 +216,7 @@ const InteractionRequest: React.FC<IInteractionRequest> = ({
);
};
const InteractionRequests = () => {
const InteractionRequestsPage = () => {
const intl = useIntl();
const { data = [], fetchNextPage, hasNextPage, isFetching, isLoading, refetch } = useFlatInteractionRequests();
@ -268,4 +268,4 @@ const InteractionRequests = () => {
);
};
export { InteractionRequests as default };
export { InteractionRequestsPage as default };

View File

@ -6,13 +6,13 @@ import ScrollableList from 'pl-fe/components/scrollable-list';
import Column from 'pl-fe/components/ui/column';
import { scheduledStatusesQueryOptions } from 'pl-fe/queries/statuses/scheduled-statuses';
import ScheduledStatus from './components/scheduled-status';
import ScheduledStatus from '../../features/scheduled-statuses/components/scheduled-status';
const messages = defineMessages({
heading: { id: 'column.scheduled_statuses', defaultMessage: 'Scheduled posts' },
});
const ScheduledStatuses = () => {
const ScheduledStatusesPage = () => {
const intl = useIntl();
const { data: scheduledStatuses = [], isLoading, hasNextPage, fetchNextPage } = useInfiniteQuery(scheduledStatusesQueryOptions);
@ -34,4 +34,4 @@ const ScheduledStatuses = () => {
);
};
export { ScheduledStatuses as default };
export { ScheduledStatusesPage as default };

View File

@ -13,9 +13,9 @@ import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { makeGetStatus } from 'pl-fe/selectors';
import { makeGetDescendantsIds } from '../status/components/thread';
import ThreadStatus from '../status/components/thread-status';
import { ComposeForm } from '../ui/util/async-components';
import { makeGetDescendantsIds } from '../../features/status/components/thread';
import ThreadStatus from '../../features/status/components/thread-status';
import { ComposeForm } from '../../features/ui/util/async-components';
import type { MediaAttachment } from 'pl-api';
import type { VirtuosoHandle } from 'react-virtuoso';
@ -28,7 +28,7 @@ interface IEventDiscussion {
onOpenVideo: (video: MediaAttachment, time: number) => void;
}
const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statusId } }) => {
const EventDiscussionPage: React.FC<IEventDiscussion> = ({ params: { statusId: statusId } }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
@ -165,4 +165,4 @@ const EventDiscussion: React.FC<IEventDiscussion> = ({ params: { statusId: statu
);
};
export { EventDiscussion as default };
export { EventDiscussionPage as default };

View File

@ -20,7 +20,7 @@ interface IEventInformation {
params: RouteParams;
}
const EventInformation: React.FC<IEventInformation> = ({ params }) => {
const EventInformationPage: React.FC<IEventInformation> = ({ params }) => {
const dispatch = useAppDispatch();
const getStatus = useCallback(makeGetStatus(), []);
const intl = useIntl();
@ -195,4 +195,4 @@ const EventInformation: React.FC<IEventInformation> = ({ params }) => {
);
};
export { EventInformation as default };
export { EventInformationPage as default };

View File

@ -13,8 +13,8 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useLoggedIn } from 'pl-fe/hooks/use-logged-in';
import { makeGetStatus } from 'pl-fe/selectors';
import Thread from './components/thread';
import ThreadLoginCta from './components/thread-login-cta';
import Thread from '../../features/status/components/thread';
import ThreadLoginCta from '../../features/status/components/thread-login-cta';
const messages = defineMessages({
title: { id: 'status.title', defaultMessage: 'Post details' },
@ -43,7 +43,7 @@ interface IStatusDetails {
params: RouteParams;
}
const StatusDetails: React.FC<IStatusDetails> = (props) => {
const StatusPage: React.FC<IStatusDetails> = (props) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { isLoggedIn } = useLoggedIn();
@ -113,4 +113,4 @@ const StatusDetails: React.FC<IStatusDetails> = (props) => {
);
};
export { StatusDetails as default };
export { StatusPage as default };