pl-fe: basic support for circles
Signed-off-by: Nicole Mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -37,6 +37,7 @@ const messages = defineMessages({
|
||||
profileDirectory: { id: 'navigation_bar.profile_directory', defaultMessage: 'Profile directory' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
circles: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
groups: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
events: { id: 'column.events', defaultMessage: 'Events' },
|
||||
dashboard: { id: 'navigation.dashboard', defaultMessage: 'Dashboard' },
|
||||
@ -283,6 +284,15 @@ const SidebarMenu: React.FC = React.memo((): JSX.Element | null => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.circles && (
|
||||
<SidebarLink
|
||||
to='/circles'
|
||||
icon={require('@tabler/icons/outline/chart-circles.svg')}
|
||||
text={intl.formatMessage(messages.circles)}
|
||||
onClick={closeSidebar}
|
||||
/>
|
||||
)}
|
||||
|
||||
{features.events && (
|
||||
<SidebarLink
|
||||
to='/events'
|
||||
|
||||
@ -26,6 +26,7 @@ const messages = defineMessages({
|
||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
circles: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
events: { id: 'column.events', defaultMessage: 'Events' },
|
||||
profileDirectory: { id: 'navigation_bar.profile_directory', defaultMessage: 'Profile directory' },
|
||||
followedTags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' },
|
||||
@ -100,6 +101,14 @@ const SidebarNavigation = React.memo(() => {
|
||||
});
|
||||
}
|
||||
|
||||
if (features.circles) {
|
||||
menu.push({
|
||||
to: '/circles',
|
||||
text: intl.formatMessage(messages.circles),
|
||||
icon: require('@tabler/icons/outline/chart-circles.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
if (features.events) {
|
||||
menu.push({
|
||||
to: '/events',
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Button from 'pl-fe/components/ui/button';
|
||||
import Form from 'pl-fe/components/ui/form';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
import Input from 'pl-fe/components/ui/input';
|
||||
import { useCreateCircle } from 'pl-fe/queries/accounts/use-circles';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: { id: 'circles.new.title_placeholder', defaultMessage: 'New circle title' },
|
||||
title: { id: 'circles.new.create', defaultMessage: 'Add circle' },
|
||||
create: { id: 'circles.new.create_title', defaultMessage: 'Add circle' },
|
||||
});
|
||||
|
||||
const NewCircleForm: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
const { mutate: createCircle, isPending } = useCreateCircle();
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTitle(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<Element>) => {
|
||||
e.preventDefault();
|
||||
createCircle(title);
|
||||
};
|
||||
|
||||
const label = intl.formatMessage(messages.label);
|
||||
const create = intl.formatMessage(messages.create);
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<label className='grow'>
|
||||
<span style={{ display: 'none' }}>{label}</span>
|
||||
|
||||
<Input
|
||||
type='text'
|
||||
value={title}
|
||||
disabled={isPending}
|
||||
onChange={handleChange}
|
||||
placeholder={label}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<Button
|
||||
disabled={isPending}
|
||||
onClick={handleSubmit}
|
||||
theme='primary'
|
||||
>
|
||||
{create}
|
||||
</Button>
|
||||
</HStack>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export { NewCircleForm as default };
|
||||
67
packages/pl-fe/src/features/circles/index.tsx
Normal file
67
packages/pl-fe/src/features/circles/index.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
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 { useCircles } from 'pl-fe/queries/accounts/use-circles';
|
||||
|
||||
import { getOrderedLists } from '../lists';
|
||||
|
||||
import NewCircleForm from './components/new-circle-form';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.circles', defaultMessage: 'Circles' },
|
||||
subheading: { id: 'circles.subheading', defaultMessage: 'Your circles' },
|
||||
});
|
||||
|
||||
const Circles: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data: circles } = useCircles(getOrderedLists);
|
||||
|
||||
if (!circles) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.circles' defaultMessage="You don't have any circles yet. When you create one, it will show up here." />;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<Stack space={4}>
|
||||
<NewCircleForm />
|
||||
|
||||
{!Object.keys(circles).length ? (
|
||||
<Card variant='rounded' size='lg'>
|
||||
{emptyMessage}
|
||||
</Card>
|
||||
) : (
|
||||
<List>
|
||||
{circles.map((circle) => (
|
||||
<ListItem
|
||||
key={circle.id}
|
||||
// to={`/circles/${circle.id}`}
|
||||
label={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/outline/list.svg')} size={20} />
|
||||
<span>{circle.title}</span>
|
||||
</HStack>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Stack>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export { Circles as default };
|
||||
@ -8,9 +8,10 @@ import { getOrderedLists } from 'pl-fe/features/lists';
|
||||
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
|
||||
import { useCompose } from 'pl-fe/hooks/use-compose';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
import { useCircles } from 'pl-fe/queries/accounts/use-circles';
|
||||
import { useLists } from 'pl-fe/queries/accounts/use-lists';
|
||||
|
||||
import type { Features } from 'pl-api';
|
||||
import type { Circle, Features } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
@ -27,6 +28,8 @@ const messages = defineMessages({
|
||||
local_long: { id: 'privacy.local.long', defaultMessage: 'Only visible on your instance' },
|
||||
list_short: { id: 'privacy.list.short', defaultMessage: 'List only' },
|
||||
list_long: { id: 'privacy.list.long', defaultMessage: 'Visible to members of a list' },
|
||||
circle_short: { id: 'privacy.circle.short', defaultMessage: 'Circle only' },
|
||||
circle_long: { id: 'privacy.circle.long', defaultMessage: 'Visible to members of a circle' },
|
||||
subscribers_short: { id: 'privacy.subscribers.short', defaultMessage: 'Subscribers-only' },
|
||||
subscribers_long: { id: 'privacy.subscribers.long', defaultMessage: 'Post to users subscribing you only' },
|
||||
|
||||
@ -42,7 +45,7 @@ interface Option {
|
||||
items?: Array<Omit<Option, 'items'>>;
|
||||
}
|
||||
|
||||
const getItems = (features: Features, lists: ReturnType<typeof getOrderedLists>, intl: IntlShape) => [
|
||||
const getItems = (features: Features, lists: ReturnType<typeof getOrderedLists>, circles: Array<Circle>, intl: IntlShape) => [
|
||||
{
|
||||
icon: require('@tabler/icons/outline/world.svg'),
|
||||
value: 'public',
|
||||
@ -96,6 +99,17 @@ const getItems = (features: Features, lists: ReturnType<typeof getOrderedLists>,
|
||||
text: intl.formatMessage(messages.list_short),
|
||||
meta: intl.formatMessage(messages.list_long),
|
||||
} as Option : undefined,
|
||||
features.circles && Object.keys(circles).length ? {
|
||||
icon: require('@tabler/icons/outline/chart-circles.svg'),
|
||||
value: '',
|
||||
items: Object.values(circles).map((circle) => ({
|
||||
icon: require('@tabler/icons/outline/list.svg'),
|
||||
value: `circle:${circle.id}`,
|
||||
text: circle.title,
|
||||
})),
|
||||
text: intl.formatMessage(messages.circle_short),
|
||||
meta: intl.formatMessage(messages.circle_long),
|
||||
} as Option : undefined,
|
||||
].filter((option): option is Option => !!option);
|
||||
|
||||
interface IPrivacyDropdown {
|
||||
@ -111,6 +125,7 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||
|
||||
const compose = useCompose(composeId);
|
||||
const { data: lists = [] } = useLists(getOrderedLists);
|
||||
const { data: circles = [] } = useCircles(getOrderedLists);
|
||||
|
||||
const value = compose.privacy;
|
||||
const unavailable = compose.id;
|
||||
@ -118,7 +133,7 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||
const onChange = (value: string) => value && dispatch(changeComposeVisibility(composeId,
|
||||
value));
|
||||
|
||||
const options = useMemo(() => getItems(features, lists, intl), [features, lists]);
|
||||
const options = useMemo(() => getItems(features, lists, circles, intl), [features, lists, circles]);
|
||||
const items: Array<MenuItem> = options.map(item => ({
|
||||
...item,
|
||||
action: item.value ? () => onChange(item.value) : undefined,
|
||||
|
||||
@ -15,11 +15,11 @@ import NewListForm from './components/new-list-form';
|
||||
import type { List as ListEntity } from 'pl-api';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
heading: { id: 'column.circles', defaultMessage: 'Lists' },
|
||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||
});
|
||||
|
||||
const getOrderedLists = (lists: Array<ListEntity>) => {
|
||||
const getOrderedLists = (lists: Array<Pick<ListEntity, 'title'>>) => {
|
||||
if (!lists) {
|
||||
return lists;
|
||||
}
|
||||
|
||||
@ -64,6 +64,7 @@ import {
|
||||
ChatIndex,
|
||||
ChatWidget,
|
||||
Circle,
|
||||
Circles,
|
||||
CommunityTimeline,
|
||||
ComposeEvent,
|
||||
Conversations,
|
||||
@ -239,6 +240,7 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = React.memo(({ chil
|
||||
|
||||
{features.lists && <WrappedRoute path='/lists' layout={DefaultLayout} component={Lists} content={children} />}
|
||||
{features.lists && <WrappedRoute path='/list/:id' layout={DefaultLayout} component={ListTimeline} content={children} />}
|
||||
{features.circles && <WrappedRoute path='/circles' layout={DefaultLayout} component={Circles} content={children} />}
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks/all' layout={DefaultLayout} component={Bookmarks} content={children} />}
|
||||
{features.bookmarks && <WrappedRoute path='/bookmarks/:id' layout={DefaultLayout} component={Bookmarks} content={children} />}
|
||||
<WrappedRoute path='/bookmarks' layout={DefaultLayout} component={BookmarkFolders} content={children} />
|
||||
|
||||
@ -14,6 +14,7 @@ export const Bookmarks = lazy(() => import('pl-fe/features/bookmarks'));
|
||||
export const BubbleTimeline = lazy(() => import('pl-fe/features/bubble-timeline'));
|
||||
export const ChatIndex = lazy(() => import('pl-fe/features/chats'));
|
||||
export const Circle = lazy(() => import('pl-fe/features/circle'));
|
||||
export const Circles = lazy(() => import('pl-fe/features/circles'));
|
||||
export const CommunityTimeline = lazy(() => import('pl-fe/features/community-timeline'));
|
||||
export const ComposeEditor = lazy(() => import('pl-fe/features/compose/editor'));
|
||||
export const ComposeEvent = lazy(() => import('pl-fe/features/compose-event'));
|
||||
|
||||
@ -314,6 +314,10 @@
|
||||
"chats.main.blankslate.title": "No messages yet",
|
||||
"chats.main.blankslate_with_chats.subtitle": "Select from one of your open chats or create a new message.",
|
||||
"chats.main.blankslate_with_chats.title": "Select a chat",
|
||||
"circles.new.create": "Add circle",
|
||||
"circles.new.create_title": "Add circle",
|
||||
"circles.new.title_placeholder": "New circle title",
|
||||
"circles.subheading": "Your circles",
|
||||
"column.admin.announcements": "Announcements",
|
||||
"column.admin.awaiting_approval": "Awaiting Approval",
|
||||
"column.admin.create_announcement": "Create announcement",
|
||||
@ -344,6 +348,7 @@
|
||||
"column.bubble": "Bubble timeline",
|
||||
"column.chats": "Chats",
|
||||
"column.circle": "Interactions circle",
|
||||
"column.circles": "Lists",
|
||||
"column.community": "Local timeline",
|
||||
"column.crypto_donate": "Donate cryptocurrency",
|
||||
"column.developers": "Developers",
|
||||
@ -725,6 +730,7 @@
|
||||
"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.",
|
||||
"empty_column.bubble": "There is nothing here! Write something publicly to fill it up",
|
||||
"empty_column.circles": "You don't have any circles yet. When you create one, it will show up here.",
|
||||
"empty_column.community": "The local timeline is empty. Write something publicly to get the ball rolling!",
|
||||
"empty_column.direct": "You don't have any direct messages yet. When you send or receive one, it will show up here.",
|
||||
"empty_column.dislikes": "No one has disliked this post yet. When someone does, they will show up here.",
|
||||
@ -1315,6 +1321,8 @@
|
||||
"preferences.options.privacy_public": "Public",
|
||||
"preferences.options.privacy_unlisted": "Unlisted",
|
||||
"privacy.change": "Adjust post privacy",
|
||||
"privacy.circle.long": "Visible to members of a circle",
|
||||
"privacy.circle.short": "Circle only",
|
||||
"privacy.direct.long": "Post to mentioned users only",
|
||||
"privacy.direct.short": "Direct",
|
||||
"privacy.list.long": "Visible to members of a list",
|
||||
|
||||
61
packages/pl-fe/src/queries/accounts/use-circles.ts
Normal file
61
packages/pl-fe/src/queries/accounts/use-circles.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useClient } from 'pl-fe/hooks/use-client';
|
||||
import { useFeatures } from 'pl-fe/hooks/use-features';
|
||||
|
||||
import { queryClient } from '../client';
|
||||
|
||||
import type { Circle } from 'pl-api';
|
||||
|
||||
const useCircles = <T>(
|
||||
select?: ((data: Array<Circle>) => T),
|
||||
) => {
|
||||
const client = useClient();
|
||||
const features = useFeatures();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['circles'],
|
||||
queryFn: () => client.circles.fetchCircles(),
|
||||
enabled: features.circles,
|
||||
select,
|
||||
});
|
||||
};
|
||||
|
||||
const useCircle = (circleId?: string) => useCircles((data) => circleId ? data.find(circle => circle.id === circleId) : undefined);
|
||||
|
||||
const useCreateCircle = () => {
|
||||
const client = useClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['circles', 'create'],
|
||||
mutationFn: (title: string) => client.circles.createCircle(title),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ['circles'] }),
|
||||
});
|
||||
};
|
||||
|
||||
const useDeleteCircle = () => {
|
||||
const client = useClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['circles', 'delete'],
|
||||
mutationFn: (circleId: string) => client.circles.deleteCircle(circleId),
|
||||
onSuccess: (_, deletedCircleId) => {
|
||||
queryClient.setQueryData<Array<Circle>>(
|
||||
['circles'],
|
||||
(prevData) => prevData?.filter(({ id }) => id !== deletedCircleId),
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const useUpdateCircle = (circleId: string) => {
|
||||
const client = useClient();
|
||||
|
||||
return useMutation({
|
||||
mutationKey: ['circles', 'update', circleId],
|
||||
mutationFn: (title: string) => client.circles.updateCircle(circleId, title),
|
||||
onSettled: () => queryClient.invalidateQueries({ queryKey: ['circles'] }),
|
||||
});
|
||||
};
|
||||
|
||||
export { useCircles, useCircle, useCreateCircle, useDeleteCircle, useUpdateCircle };
|
||||
Reference in New Issue
Block a user