Allow addressing posts to lists
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
f3ec08777c
commit
4841c5b0a2
8 changed files with 168 additions and 59 deletions
|
@ -21,6 +21,7 @@ interface MenuItem {
|
||||||
text: string;
|
text: string;
|
||||||
to?: string;
|
to?: string;
|
||||||
type?: 'toggle';
|
type?: 'toggle';
|
||||||
|
items?: Array<Omit<MenuItem, 'items'>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDropdownMenuItem {
|
interface IDropdownMenuItem {
|
||||||
|
@ -28,9 +29,10 @@ interface IDropdownMenuItem {
|
||||||
item: MenuItem | null;
|
item: MenuItem | null;
|
||||||
onClick?(goBack?: boolean): void;
|
onClick?(goBack?: boolean): void;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
onSetTab: (tab?: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropdownMenuItem = ({ index, item, onClick, autoFocus }: IDropdownMenuItem) => {
|
const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdownMenuItem) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const itemRef = useRef<HTMLAnchorElement>(null);
|
const itemRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
@ -40,6 +42,12 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus }: IDropdownMenuItem
|
||||||
|
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
|
if (item.items?.length) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSetTab(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (onClick) onClick(!(item.to && userTouching.matches));
|
if (onClick) onClick(!(item.to && userTouching.matches));
|
||||||
|
|
||||||
if (item.to) {
|
if (item.to) {
|
||||||
|
@ -112,7 +120,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus }: IDropdownMenuItem
|
||||||
>
|
>
|
||||||
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
{item.icon && <Icon src={item.icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||||
|
|
||||||
<div className={clsx('text-xs', { 'mr-2': item.count || item.type === 'toggle' })}>
|
<div className={clsx('text-xs', { 'mr-2': item.count || item.type === 'toggle' || item.items?.length })}>
|
||||||
<div className='truncate text-base'>{item.text}</div>
|
<div className='truncate text-base'>{item.text}</div>
|
||||||
<div className='mt-0.5'>{item.meta}</div>
|
<div className='mt-0.5'>{item.meta}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -128,6 +136,10 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus }: IDropdownMenuItem
|
||||||
<Toggle checked={item.checked} onChange={handleChange} />
|
<Toggle checked={item.checked} onChange={handleChange} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!!item.items?.length && (
|
||||||
|
<Icon src={require('@tabler/icons/outline/chevron-right.svg')} containerClassName='ml-auto' className='h-5 w-5 flex-none' />
|
||||||
|
)}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,12 +4,13 @@ import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
|
|
||||||
import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'pl-fe/actions/dropdown-menu';
|
import { closeDropdownMenu as closeDropdownMenuRedux, openDropdownMenu } from 'pl-fe/actions/dropdown-menu';
|
||||||
import { useAppDispatch } from 'pl-fe/hooks';
|
import { useAppDispatch } from 'pl-fe/hooks';
|
||||||
import { userTouching } from 'pl-fe/is-mobile';
|
import { userTouching } from 'pl-fe/is-mobile';
|
||||||
|
|
||||||
import { IconButton, Portal } from '../ui';
|
import { HStack, IconButton, Portal } from '../ui';
|
||||||
|
|
||||||
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
|
import DropdownMenuItem, { MenuItem } from './dropdown-menu-item';
|
||||||
|
|
||||||
|
@ -49,6 +50,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
const [isDisplayed, setIsDisplayed] = useState<boolean>(false);
|
const [isDisplayed, setIsDisplayed] = useState<boolean>(false);
|
||||||
|
const [tab, setTab] = useState<number>();
|
||||||
|
|
||||||
const touching = userTouching.matches;
|
const touching = userTouching.matches;
|
||||||
|
|
||||||
|
@ -92,6 +94,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
const handleOpen = () => {
|
const handleOpen = () => {
|
||||||
dispatch(openDropdownMenu());
|
dispatch(openDropdownMenu());
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
setTab(undefined);
|
||||||
|
|
||||||
if (onOpen) {
|
if (onOpen) {
|
||||||
onOpen();
|
onOpen();
|
||||||
|
@ -147,6 +150,11 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
}
|
}
|
||||||
}, [refs.floating.current]);
|
}, [refs.floating.current]);
|
||||||
|
|
||||||
|
const handleExitSubmenu: React.EventHandler<any> = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
setTab(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = useMemo(() => (e: KeyboardEvent) => {
|
const handleKeyDown = useMemo(() => (e: KeyboardEvent) => {
|
||||||
if (!refs.floating.current) return;
|
if (!refs.floating.current) return;
|
||||||
|
|
||||||
|
@ -156,6 +164,9 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
let element = null;
|
let element = null;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
|
case 'ArrowLeft':
|
||||||
|
if (tab !== undefined) setTab(undefined);
|
||||||
|
break;
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
element = items[index + 1] || items[0];
|
element = items[index + 1] || items[0];
|
||||||
break;
|
break;
|
||||||
|
@ -280,6 +291,21 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
return className;
|
return className;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderItems = (items: Menu | undefined) => (
|
||||||
|
<ul>
|
||||||
|
{items?.map((item, idx) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={idx}
|
||||||
|
item={item}
|
||||||
|
index={idx}
|
||||||
|
onClick={handleClose}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
onSetTab={setTab}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{children ? (
|
{children ? (
|
||||||
|
@ -325,29 +351,45 @@ const DropdownMenu = (props: IDropdownMenu) => {
|
||||||
left: x ?? 0,
|
left: x ?? 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Component && <Component handleClose={handleClose} />}
|
{items?.some(item => item?.items?.length) ? (
|
||||||
{(items?.length || touching) && (
|
<ReactSwipeableViews animateHeight index={tab === undefined ? 0 : 1}>
|
||||||
<ul>
|
<div className={clsx({ 'w-full': touching })}>
|
||||||
{items?.map((item, idx) => (
|
{Component && <Component handleClose={handleClose} />}
|
||||||
<DropdownMenuItem
|
{(items?.length || touching) && renderItems(items)}
|
||||||
key={idx}
|
</div>
|
||||||
item={item}
|
<div className={clsx({ 'w-full': touching, 'fit-content': !touching })}>
|
||||||
index={idx}
|
{tab !== undefined && (
|
||||||
onClick={handleClose}
|
<>
|
||||||
autoFocus={autoFocus}
|
<HStack className='mx-2 my-1 text-gray-700 dark:text-gray-300' space={3} alignItems='center'>
|
||||||
/>
|
<IconButton
|
||||||
))}
|
theme='transparent'
|
||||||
{touching && (
|
src={require('@tabler/icons/outline/arrow-left.svg')}
|
||||||
<li className='p-2 px-3'>
|
iconClassName='h-5 w-5'
|
||||||
<button
|
onClick={handleExitSubmenu}
|
||||||
className='flex w-full appearance-none place-content-center items-center justify-center rounded-full border border-gray-700 bg-transparent p-2 text-sm font-medium text-gray-700 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:border-gray-500 dark:text-gray-500'
|
/>
|
||||||
onClick={handleClose}
|
{items![tab]?.text}
|
||||||
>
|
</HStack>
|
||||||
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
|
{renderItems(items![tab]?.items)}
|
||||||
</button>
|
</>
|
||||||
</li>
|
)}
|
||||||
)}
|
</div>
|
||||||
</ul>
|
</ReactSwipeableViews>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{Component && <Component handleClose={handleClose} />}
|
||||||
|
{(items?.length || touching) && renderItems(items)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{touching && (
|
||||||
|
<div className='p-2 px-3'>
|
||||||
|
<button
|
||||||
|
className='flex w-full appearance-none place-content-center items-center justify-center rounded-full border border-gray-700 bg-transparent p-2 text-sm font-medium text-gray-700 transition-all hover:bg-white/10 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:border-gray-500 dark:text-gray-500'
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Arrow */}
|
{/* Arrow */}
|
||||||
|
|
|
@ -8,7 +8,6 @@ interface IIconButton extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||||
expanded?: boolean;
|
expanded?: boolean;
|
||||||
iconClassName?: string;
|
iconClassName?: string;
|
||||||
pressed?: boolean;
|
pressed?: boolean;
|
||||||
size?: number;
|
|
||||||
src: string;
|
src: string;
|
||||||
text?: React.ReactNode;
|
text?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
@ -27,7 +26,6 @@ const IconButton: React.FC<IIconButton> = ({
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
onMouseLeave,
|
onMouseLeave,
|
||||||
pressed,
|
pressed,
|
||||||
size = 18,
|
|
||||||
src,
|
src,
|
||||||
tabIndex = 0,
|
tabIndex = 0,
|
||||||
text,
|
text,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import Counter from '../counter/counter';
|
import Counter from '../counter/counter';
|
||||||
|
@ -7,6 +8,8 @@ import SvgIcon from './svg-icon';
|
||||||
interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
|
interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
|
||||||
/** Class name for the <svg> element. */
|
/** Class name for the <svg> element. */
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/** Class name for the <div> element. */
|
||||||
|
containerClassName?: string;
|
||||||
/** Number to display a counter over the icon. */
|
/** Number to display a counter over the icon. */
|
||||||
count?: number;
|
count?: number;
|
||||||
/** Optional max to cap count (ie: N+) */
|
/** Optional max to cap count (ie: N+) */
|
||||||
|
@ -22,9 +25,9 @@ interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Renders and SVG icon with optional counter. */
|
/** Renders and SVG icon with optional counter. */
|
||||||
const Icon: React.FC<IIcon> = ({ src, alt, count, size, countMax, ...filteredProps }): JSX.Element => (
|
const Icon: React.FC<IIcon> = ({ src, alt, count, size, countMax, containerClassName, ...filteredProps }): JSX.Element => (
|
||||||
<div
|
<div
|
||||||
className='relative flex shrink-0 flex-col'
|
className={clsx('relative flex shrink-0 flex-col', containerClassName)}
|
||||||
data-testid={filteredProps['data-testid'] || 'icon'}
|
data-testid={filteredProps['data-testid'] || 'icon'}
|
||||||
>
|
>
|
||||||
{count ? (
|
{count ? (
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
import { useIntl, defineMessages, IntlShape } from 'react-intl';
|
import { useIntl, defineMessages, IntlShape } from 'react-intl';
|
||||||
|
|
||||||
import { changeComposeFederated, changeComposeVisibility } from 'pl-fe/actions/compose';
|
import { changeComposeFederated, changeComposeVisibility } from 'pl-fe/actions/compose';
|
||||||
|
import { fetchLists } from 'pl-fe/actions/lists';
|
||||||
import DropdownMenu, { MenuItem } from 'pl-fe/components/dropdown-menu';
|
import DropdownMenu, { MenuItem } from 'pl-fe/components/dropdown-menu';
|
||||||
import { Button } from 'pl-fe/components/ui';
|
import { Button } from 'pl-fe/components/ui';
|
||||||
import { useAppDispatch, useCompose, useFeatures } from 'pl-fe/hooks';
|
import { getOrderedLists } from 'pl-fe/features/lists';
|
||||||
|
import { useAppDispatch, useAppSelector, useCompose, useFeatures } from 'pl-fe/hooks';
|
||||||
|
|
||||||
import type { Features } from 'pl-api';
|
import type { Features } from 'pl-api';
|
||||||
|
|
||||||
|
@ -21,6 +23,8 @@ const messages = defineMessages({
|
||||||
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
direct_long: { id: 'privacy.direct.long', defaultMessage: 'Post to mentioned users only' },
|
||||||
local_short: { id: 'privacy.local.short', defaultMessage: 'Local-only' },
|
local_short: { id: 'privacy.local.short', defaultMessage: 'Local-only' },
|
||||||
local_long: { id: 'privacy.local.long', defaultMessage: 'Only visible on your instance' },
|
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' },
|
||||||
|
|
||||||
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' },
|
change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust post privacy' },
|
||||||
local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' },
|
local: { id: 'privacy.local', defaultMessage: '{privacy} (local-only)' },
|
||||||
|
@ -30,16 +34,58 @@ interface Option {
|
||||||
icon: string;
|
icon: string;
|
||||||
value: string;
|
value: string;
|
||||||
text: string;
|
text: string;
|
||||||
meta: string;
|
meta?: string;
|
||||||
|
items?: Array<Omit<Option, 'items'>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getItems = (features: Features, intl: IntlShape) => [
|
const getItems = (features: Features, lists: ReturnType<typeof getOrderedLists>, intl: IntlShape) => [
|
||||||
{ icon: require('@tabler/icons/outline/world.svg'), value: 'public', text: intl.formatMessage(messages.public_short), meta: intl.formatMessage(messages.public_long) },
|
{
|
||||||
{ icon: require('@tabler/icons/outline/lock-open.svg'), value: 'unlisted', text: intl.formatMessage(messages.unlisted_short), meta: intl.formatMessage(messages.unlisted_long) },
|
icon: require('@tabler/icons/outline/world.svg'),
|
||||||
{ icon: require('@tabler/icons/outline/lock.svg'), value: 'private', text: intl.formatMessage(messages.private_short), meta: intl.formatMessage(messages.private_long) },
|
value: 'public',
|
||||||
features.visibilityMutualsOnly ? { icon: require('@tabler/icons/outline/users-group.svg'), value: 'mutuals_only', text: intl.formatMessage(messages.mutuals_only_short), meta: intl.formatMessage(messages.mutuals_only_long) } : undefined,
|
text: intl.formatMessage(messages.public_short),
|
||||||
{ icon: require('@tabler/icons/outline/mail.svg'), value: 'direct', text: intl.formatMessage(messages.direct_short), meta: intl.formatMessage(messages.direct_long) },
|
meta: intl.formatMessage(messages.public_long),
|
||||||
features.visibilityLocalOnly ? { icon: require('@tabler/icons/outline/affiliate.svg'), value: 'local', text: intl.formatMessage(messages.local_short), meta: intl.formatMessage(messages.local_long) } : undefined,
|
},
|
||||||
|
{
|
||||||
|
icon: require('@tabler/icons/outline/lock-open.svg'),
|
||||||
|
value: 'unlisted',
|
||||||
|
text: intl.formatMessage(messages.unlisted_short),
|
||||||
|
meta: intl.formatMessage(messages.unlisted_long),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: require('@tabler/icons/outline/lock.svg'),
|
||||||
|
value: 'private',
|
||||||
|
text: intl.formatMessage(messages.private_short),
|
||||||
|
meta: intl.formatMessage(messages.private_long),
|
||||||
|
},
|
||||||
|
features.visibilityMutualsOnly ? {
|
||||||
|
icon: require('@tabler/icons/outline/users-group.svg'),
|
||||||
|
value: 'mutuals_only',
|
||||||
|
text: intl.formatMessage(messages.mutuals_only_short),
|
||||||
|
meta: intl.formatMessage(messages.mutuals_only_long),
|
||||||
|
} : undefined,
|
||||||
|
{
|
||||||
|
icon: require('@tabler/icons/outline/mail.svg'),
|
||||||
|
value: 'direct',
|
||||||
|
text: intl.formatMessage(messages.direct_short),
|
||||||
|
meta: intl.formatMessage(messages.direct_long),
|
||||||
|
},
|
||||||
|
features.visibilityLocalOnly ? {
|
||||||
|
icon: require('@tabler/icons/outline/affiliate.svg'),
|
||||||
|
value: 'local',
|
||||||
|
text: intl.formatMessage(messages.local_short),
|
||||||
|
meta: intl.formatMessage(messages.local_long),
|
||||||
|
} : undefined,
|
||||||
|
features.addressableLists && !lists.isEmpty() ? {
|
||||||
|
icon: require('@tabler/icons/outline/list.svg'),
|
||||||
|
value: '',
|
||||||
|
items: lists.toArray().map((list) => ({
|
||||||
|
icon: require('@tabler/icons/outline/list.svg'),
|
||||||
|
value: `list:${list.id}`,
|
||||||
|
text: list.title,
|
||||||
|
})),
|
||||||
|
text: intl.formatMessage(messages.list_short),
|
||||||
|
meta: intl.formatMessage(messages.list_long),
|
||||||
|
} as Option : undefined,
|
||||||
].filter((option): option is Option => !!option);
|
].filter((option): option is Option => !!option);
|
||||||
|
|
||||||
interface IPrivacyDropdown {
|
interface IPrivacyDropdown {
|
||||||
|
@ -54,14 +100,29 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const compose = useCompose(composeId);
|
const compose = useCompose(composeId);
|
||||||
|
const lists = useAppSelector((state) => getOrderedLists(state));
|
||||||
|
|
||||||
const value = compose.privacy;
|
const value = compose.privacy;
|
||||||
const unavailable = compose.id;
|
const unavailable = compose.id;
|
||||||
|
|
||||||
const onChange = (value: string) => value && dispatch(changeComposeVisibility(composeId, value));
|
const onChange = (value: string) => value && dispatch(changeComposeVisibility(composeId,
|
||||||
|
value));
|
||||||
|
|
||||||
const options = getItems(features, intl);
|
const options = useMemo(() => getItems(features, lists, intl), [features, lists]);
|
||||||
const items: Array<MenuItem> = options.map(item => ({ ...item, action: () => onChange(item.value), active: item.value === value }));
|
const items: Array<MenuItem> = options.map(item => ({
|
||||||
|
...item,
|
||||||
|
action: item.value ? () => onChange(item.value) : undefined,
|
||||||
|
active: item.value === value || item.items?.some((item) => item.value === value),
|
||||||
|
items: item.items?.map(item => ({
|
||||||
|
...item,
|
||||||
|
action: item.value ? () => onChange(item.value) : undefined,
|
||||||
|
active: item.value === value,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (features.addressableLists) dispatch(fetchLists());
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (features.localOnlyStatuses) items.push({
|
if (features.localOnlyStatuses) items.push({
|
||||||
icon: require('@tabler/icons/outline/affiliate.svg'),
|
icon: require('@tabler/icons/outline/affiliate.svg'),
|
||||||
|
@ -72,12 +133,15 @@ const PrivacyDropdown: React.FC<IPrivacyDropdown> = ({
|
||||||
onChange: () => dispatch(changeComposeFederated(composeId)),
|
onChange: () => dispatch(changeComposeFederated(composeId)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const valueOption = useMemo(() => [
|
||||||
|
options,
|
||||||
|
options.filter(option => option.items).map(option => option.items).flat(),
|
||||||
|
].flat().find(item => item!.value === value), [value]);
|
||||||
|
|
||||||
if (unavailable) {
|
if (unavailable) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const valueOption = options.find(item => item.value === value);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu items={items}>
|
<DropdownMenu items={items}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -75,4 +75,4 @@ const Lists: React.FC = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Lists as default };
|
export { Lists as default, getOrderedLists };
|
||||||
|
|
|
@ -1,34 +1,22 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
import { setupListAdder, resetListAdder } from 'pl-fe/actions/lists';
|
import { setupListAdder, resetListAdder } from 'pl-fe/actions/lists';
|
||||||
import { CardHeader, CardTitle, Modal } from 'pl-fe/components/ui';
|
import { CardHeader, CardTitle, Modal } from 'pl-fe/components/ui';
|
||||||
import AccountContainer from 'pl-fe/containers/account-container';
|
import AccountContainer from 'pl-fe/containers/account-container';
|
||||||
|
import { getOrderedLists } from 'pl-fe/features/lists';
|
||||||
import NewListForm from 'pl-fe/features/lists/components/new-list-form';
|
import NewListForm from 'pl-fe/features/lists/components/new-list-form';
|
||||||
import { useAppDispatch, useAppSelector } from 'pl-fe/hooks';
|
import { useAppDispatch, useAppSelector } from 'pl-fe/hooks';
|
||||||
|
|
||||||
import List from './components/list';
|
import List from './components/list';
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
|
||||||
import type { List as ListEntity } from 'pl-api';
|
|
||||||
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
|
||||||
import type { RootState } from 'pl-fe/store';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
subheading: { id: 'lists.subheading', defaultMessage: 'Your lists' },
|
||||||
add: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
add: { id: 'lists.new.create', defaultMessage: 'Add list' },
|
||||||
});
|
});
|
||||||
|
|
||||||
// hack
|
|
||||||
const getOrderedLists = createSelector([(state: RootState) => state.lists], lists => {
|
|
||||||
if (!lists) {
|
|
||||||
return lists;
|
|
||||||
}
|
|
||||||
|
|
||||||
return lists.toList().filter(item => !!item).sort((a, b) => (a as ListEntity).title.localeCompare((b as ListEntity).title)) as ImmutableList<ListEntity>;
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ListAdderModalProps {
|
interface ListAdderModalProps {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1269,6 +1269,8 @@
|
||||||
"privacy.change": "Adjust post privacy",
|
"privacy.change": "Adjust post privacy",
|
||||||
"privacy.direct.long": "Post to mentioned users only",
|
"privacy.direct.long": "Post to mentioned users only",
|
||||||
"privacy.direct.short": "Direct",
|
"privacy.direct.short": "Direct",
|
||||||
|
"privacy.list.long": "Visible to members of a list",
|
||||||
|
"privacy.list.short": "List only",
|
||||||
"privacy.local": "{privacy} (local-only)",
|
"privacy.local": "{privacy} (local-only)",
|
||||||
"privacy.local.long": "Only visible on your instance",
|
"privacy.local.long": "Only visible on your instance",
|
||||||
"privacy.local.short": "Local-only",
|
"privacy.local.short": "Local-only",
|
||||||
|
|
Loading…
Reference in a new issue