diff --git a/packages/pl-fe/src/components/sidebar-menu.tsx b/packages/pl-fe/src/components/sidebar-menu.tsx
index 7ae1f5a23..aaa3fd4d5 100644
--- a/packages/pl-fe/src/components/sidebar-menu.tsx
+++ b/packages/pl-fe/src/components/sidebar-menu.tsx
@@ -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 && (
+
+ )}
+
{features.events && (
{
});
}
+ 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',
diff --git a/packages/pl-fe/src/features/circles/components/new-circle-form.tsx b/packages/pl-fe/src/features/circles/components/new-circle-form.tsx
new file mode 100644
index 000000000..1e6b9341b
--- /dev/null
+++ b/packages/pl-fe/src/features/circles/components/new-circle-form.tsx
@@ -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) => {
+ setTitle(e.target.value);
+ };
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ createCircle(title);
+ };
+
+ const label = intl.formatMessage(messages.label);
+ const create = intl.formatMessage(messages.create);
+
+ return (
+
+ );
+};
+
+export { NewCircleForm as default };
diff --git a/packages/pl-fe/src/features/circles/index.tsx b/packages/pl-fe/src/features/circles/index.tsx
new file mode 100644
index 000000000..b46e48ad8
--- /dev/null
+++ b/packages/pl-fe/src/features/circles/index.tsx
@@ -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 (
+
+
+
+ );
+ }
+
+ const emptyMessage = ;
+
+ return (
+
+
+
+
+ {!Object.keys(circles).length ? (
+
+ {emptyMessage}
+
+ ) : (
+
+ {circles.map((circle) => (
+
+
+ {circle.title}
+
+ }
+ />
+ ))}
+
+ )}
+
+
+ );
+};
+
+export { Circles as default };
diff --git a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx
index 332aaf8ac..7ce95950c 100644
--- a/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx
+++ b/packages/pl-fe/src/features/compose/components/privacy-dropdown.tsx
@@ -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>;
}
-const getItems = (features: Features, lists: ReturnType, intl: IntlShape) => [
+const getItems = (features: Features, lists: ReturnType, circles: Array, intl: IntlShape) => [
{
icon: require('@tabler/icons/outline/world.svg'),
value: 'public',
@@ -96,6 +99,17 @@ const getItems = (features: Features, lists: ReturnType,
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 = ({
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 = ({
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