Allow adding an account to a collection directly from the profile page (#39080)
This commit is contained in:
parent
a444a0b572
commit
6e7e8de343
@ -21,6 +21,10 @@ import {
|
|||||||
import { openModal } from '@/mastodon/actions/modal';
|
import { openModal } from '@/mastodon/actions/modal';
|
||||||
import { initMuteModal } from '@/mastodon/actions/mutes';
|
import { initMuteModal } from '@/mastodon/actions/mutes';
|
||||||
import { initReport } from '@/mastodon/actions/reports';
|
import { initReport } from '@/mastodon/actions/reports';
|
||||||
|
import {
|
||||||
|
canAccountBeAdded,
|
||||||
|
canAccountBeAddedByFollowers,
|
||||||
|
} from '@/mastodon/features/collections/utils';
|
||||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||||
import { useIdentity } from '@/mastodon/identity_context';
|
import { useIdentity } from '@/mastodon/identity_context';
|
||||||
import type { Account } from '@/mastodon/models/account';
|
import type { Account } from '@/mastodon/models/account';
|
||||||
@ -214,6 +218,10 @@ const redesignMessages = defineMessages({
|
|||||||
id: 'account.menu.add_to_list',
|
id: 'account.menu.add_to_list',
|
||||||
defaultMessage: 'Add to list…',
|
defaultMessage: 'Add to list…',
|
||||||
},
|
},
|
||||||
|
addToCollection: {
|
||||||
|
id: 'account.menu.add_to_collection',
|
||||||
|
defaultMessage: 'Add to collection…',
|
||||||
|
},
|
||||||
openOriginalPage: {
|
openOriginalPage: {
|
||||||
id: 'account.menu.open_original_page',
|
id: 'account.menu.open_original_page',
|
||||||
defaultMessage: 'View on {domain}',
|
defaultMessage: 'View on {domain}',
|
||||||
@ -294,35 +302,57 @@ function getMenuItems({
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
// List and featuring options
|
// Add to list
|
||||||
if (relationship?.following) {
|
if (relationship?.following) {
|
||||||
items.push(
|
items.push({
|
||||||
{
|
text: intl.formatMessage(redesignMessages.addToList),
|
||||||
text: intl.formatMessage(redesignMessages.addToList),
|
action: () => {
|
||||||
action: () => {
|
dispatch(
|
||||||
dispatch(
|
openModal({
|
||||||
openModal({
|
modalType: 'LIST_ADDER',
|
||||||
modalType: 'LIST_ADDER',
|
modalProps: {
|
||||||
modalProps: {
|
accountId: account.id,
|
||||||
accountId: account.id,
|
},
|
||||||
},
|
}),
|
||||||
}),
|
);
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
text: intl.formatMessage(
|
}
|
||||||
relationship.endorsed ? messages.unendorse : messages.endorse,
|
|
||||||
),
|
// Add to collection
|
||||||
action: () => {
|
if (
|
||||||
if (relationship.endorsed) {
|
canAccountBeAdded(account) ||
|
||||||
dispatch(unpinAccount(account.id));
|
(canAccountBeAddedByFollowers(account) && relationship?.following)
|
||||||
} else {
|
) {
|
||||||
dispatch(pinAccount(account.id));
|
items.push({
|
||||||
}
|
text: intl.formatMessage(redesignMessages.addToCollection),
|
||||||
},
|
action: () => {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'COLLECTION_ADDER',
|
||||||
|
modalProps: {
|
||||||
|
accountId: account.id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature on profile
|
||||||
|
if (relationship?.following) {
|
||||||
|
items.push({
|
||||||
|
text: intl.formatMessage(
|
||||||
|
relationship.endorsed ? messages.unendorse : messages.endorse,
|
||||||
|
),
|
||||||
|
action: () => {
|
||||||
|
if (relationship.endorsed) {
|
||||||
|
dispatch(unpinAccount(account.id));
|
||||||
|
} else {
|
||||||
|
dispatch(pinAccount(account.id));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
|
|||||||
@ -0,0 +1,12 @@
|
|||||||
|
.wrapper {
|
||||||
|
--list-item-gap: 16px;
|
||||||
|
--list-item-padding-block: 12px;
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: 1px solid var(--color-border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(input:disabled) {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { useId } from 'react';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||||
|
import { Toggle } from '@/mastodon/components/form_fields';
|
||||||
|
import {
|
||||||
|
ListItemContent,
|
||||||
|
ListItemWrapper,
|
||||||
|
} from '@/mastodon/components/list_item';
|
||||||
|
import {
|
||||||
|
AvatarGrid,
|
||||||
|
CollectionInfo,
|
||||||
|
} from 'mastodon/features/collections/components/collection_lockup';
|
||||||
|
|
||||||
|
import classes from './collection_toggle.module.scss';
|
||||||
|
|
||||||
|
export interface CollectionToggleProps {
|
||||||
|
collection: ApiCollectionJSON;
|
||||||
|
checked: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CollectionToggle: React.FC<CollectionToggleProps> = ({
|
||||||
|
collection,
|
||||||
|
checked,
|
||||||
|
disabled,
|
||||||
|
subtitle,
|
||||||
|
onChange,
|
||||||
|
}) => {
|
||||||
|
const uniqueId = useId();
|
||||||
|
const toggleId = `${uniqueId}-toggle`;
|
||||||
|
const infoId = `${uniqueId}-info`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemWrapper
|
||||||
|
className={classes.wrapper}
|
||||||
|
icon={
|
||||||
|
<AvatarGrid
|
||||||
|
accountIds={collection.items.map((item) => item.account_id)}
|
||||||
|
sensitive={collection.sensitive}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sideContent={
|
||||||
|
<Toggle
|
||||||
|
id={toggleId}
|
||||||
|
checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={onChange}
|
||||||
|
aria-describedby={infoId}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ListItemContent
|
||||||
|
as='label'
|
||||||
|
htmlFor={toggleId}
|
||||||
|
subtitle={
|
||||||
|
subtitle ?? (
|
||||||
|
<CollectionInfo
|
||||||
|
collection={collection}
|
||||||
|
withTimestamp={false}
|
||||||
|
withAuthorHandle={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{collection.name}
|
||||||
|
</ListItemContent>
|
||||||
|
</ListItemWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
143
app/javascript/mastodon/features/collection_adder/index.tsx
Normal file
143
app/javascript/mastodon/features/collection_adder/index.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { useCallback, useId, useState } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import type { ApiCollectionJSON } from '@/mastodon/api_types/collections';
|
||||||
|
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||||
|
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||||
|
import type { Account } from '@/mastodon/models/account';
|
||||||
|
import {
|
||||||
|
addCollectionItem,
|
||||||
|
removeCollectionItem,
|
||||||
|
} from '@/mastodon/reducers/slices/collections';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { MAX_COLLECTION_ACCOUNT_COUNT } from '../collections/editor/accounts';
|
||||||
|
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
|
||||||
|
|
||||||
|
import { CollectionToggle } from './collection_toggle';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: {
|
||||||
|
id: 'lightbox.close',
|
||||||
|
defaultMessage: 'Close',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListItem: React.FC<{
|
||||||
|
collection: ApiCollectionJSON;
|
||||||
|
account: Account;
|
||||||
|
}> = ({ collection, account }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
const accountItemInCollection = collection.items.find(
|
||||||
|
(item) => item.account_id === account.id,
|
||||||
|
);
|
||||||
|
const isAccountInCollection = !!accountItemInCollection;
|
||||||
|
|
||||||
|
const addOrRemove = useCallback(
|
||||||
|
async (shouldAdd: boolean) => {
|
||||||
|
setIsUpdating(true);
|
||||||
|
|
||||||
|
if (shouldAdd) {
|
||||||
|
await dispatch(
|
||||||
|
addCollectionItem({
|
||||||
|
collectionId: collection.id,
|
||||||
|
accountId: account.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else if (accountItemInCollection) {
|
||||||
|
await dispatch(
|
||||||
|
removeCollectionItem({
|
||||||
|
collectionId: collection.id,
|
||||||
|
itemId: accountItemInCollection.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUpdating(false);
|
||||||
|
},
|
||||||
|
[account.id, collection.id, accountItemInCollection, dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
void addOrRemove(e.target.checked);
|
||||||
|
},
|
||||||
|
[addOrRemove],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasMaxItemCount =
|
||||||
|
!isAccountInCollection &&
|
||||||
|
collection.item_count >= MAX_COLLECTION_ACCOUNT_COUNT;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollectionToggle
|
||||||
|
key={collection.id}
|
||||||
|
collection={collection}
|
||||||
|
disabled={isUpdating || hasMaxItemCount}
|
||||||
|
subtitle={
|
||||||
|
hasMaxItemCount ? (
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.search_accounts_max_reached'
|
||||||
|
defaultMessage='You have added the maximum number of accounts'
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
checked={isAccountInCollection}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CollectionAdder: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
}> = ({ accountId, onClose }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const titleId = useId();
|
||||||
|
const account = useAppSelector((state) => state.accounts.get(accountId));
|
||||||
|
const currentAccountId = useCurrentAccountId();
|
||||||
|
const { collections, status } = useCollectionsCreatedBy(currentAccountId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='modal-root__modal dialog-modal'>
|
||||||
|
<div className='dialog-modal__header'>
|
||||||
|
<IconButton
|
||||||
|
className='dialog-modal__header__close'
|
||||||
|
title={intl.formatMessage(messages.close)}
|
||||||
|
icon='times'
|
||||||
|
iconComponent={CloseIcon}
|
||||||
|
onClick={onClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className='dialog-modal__header__title' id={titleId}>
|
||||||
|
<FormattedMessage
|
||||||
|
id='collections.add_to_collection'
|
||||||
|
defaultMessage='Add {name} to collections'
|
||||||
|
values={{ name: <strong>@{account?.acct}</strong> }}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='dialog-modal__content'>
|
||||||
|
<div
|
||||||
|
className='lists-scrollable'
|
||||||
|
role='group'
|
||||||
|
aria-labelledby={titleId}
|
||||||
|
>
|
||||||
|
{status === 'loading' || !account ? (
|
||||||
|
<LoadingIndicator />
|
||||||
|
) : (
|
||||||
|
collections.map((item) => (
|
||||||
|
<ListItem key={item.id} collection={item} account={account} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -57,10 +57,45 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { id, name } = collection;
|
const { id, name } = collection;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItemWrapper
|
||||||
|
className={classNames(classes.wrapper, className)}
|
||||||
|
icon={
|
||||||
|
<AvatarGrid
|
||||||
|
accountIds={collection.items.map((item) => item.account_id)}
|
||||||
|
sensitive={collection.sensitive}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sideContent={sideContent}
|
||||||
|
>
|
||||||
|
<ListItemLink
|
||||||
|
as='h3'
|
||||||
|
to={getCollectionPath(id)}
|
||||||
|
subtitle={
|
||||||
|
<CollectionInfo
|
||||||
|
collection={collection}
|
||||||
|
withAuthorHandle={withAuthorHandle}
|
||||||
|
withTimestamp={withTimestamp}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</ListItemLink>
|
||||||
|
</ListItemWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CollectionInfo: React.FC<
|
||||||
|
Pick<
|
||||||
|
CollectionLockupProps,
|
||||||
|
'collection' | 'withAuthorHandle' | 'withTimestamp'
|
||||||
|
>
|
||||||
|
> = ({ collection, withAuthorHandle, withTimestamp }) => {
|
||||||
const authorAccount = useAccount(collection.account_id);
|
const authorAccount = useAccount(collection.account_id);
|
||||||
const authorHandle = useAccountHandle(authorAccount, domain);
|
const authorHandle = useAccountHandle(authorAccount, domain);
|
||||||
|
|
||||||
const collectionInfo = (
|
return (
|
||||||
<ul>
|
<ul>
|
||||||
{collection.sensitive && (
|
{collection.sensitive && (
|
||||||
<li className='sr-only'>
|
<li className='sr-only'>
|
||||||
@ -98,25 +133,4 @@ export const CollectionLockup: React.FC<CollectionLockupProps> = ({
|
|||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItemWrapper
|
|
||||||
className={classNames(classes.wrapper, className)}
|
|
||||||
icon={
|
|
||||||
<AvatarGrid
|
|
||||||
accountIds={collection.items.map((item) => item.account_id)}
|
|
||||||
sensitive={collection.sensitive}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
sideContent={sideContent}
|
|
||||||
>
|
|
||||||
<ListItemLink
|
|
||||||
as='h3'
|
|
||||||
to={getCollectionPath(id)}
|
|
||||||
subtitle={collectionInfo}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</ListItemLink>
|
|
||||||
</ListItemWrapper>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,11 +39,12 @@ import {
|
|||||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
import { PendingNote } from '../detail';
|
import { PendingNote } from '../detail';
|
||||||
|
import { canAccountBeAdded, canAccountBeAddedByFollowers } from '../utils';
|
||||||
|
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
import { WizardStepTitle } from './wizard_step_title';
|
import { WizardStepTitle } from './wizard_step_title';
|
||||||
|
|
||||||
const MAX_ACCOUNT_COUNT = 25;
|
export const MAX_COLLECTION_ACCOUNT_COUNT = 25;
|
||||||
|
|
||||||
const AddedAccountItem: React.FC<{
|
const AddedAccountItem: React.FC<{
|
||||||
accountId: string;
|
accountId: string;
|
||||||
@ -99,9 +100,6 @@ const renderAccountItem = (account: ApiMutedAccountJSON) => (
|
|||||||
|
|
||||||
type GroupKey = 'available' | 'mustFollow' | 'disabled';
|
type GroupKey = 'available' | 'mustFollow' | 'disabled';
|
||||||
|
|
||||||
const canAccountBeAdded = (account: ApiMutedAccountJSON) =>
|
|
||||||
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
|
||||||
|
|
||||||
function groupSuggestions(
|
function groupSuggestions(
|
||||||
accounts: ApiMutedAccountJSON[],
|
accounts: ApiMutedAccountJSON[],
|
||||||
relationships: ImmutableMap<string, Relationship>,
|
relationships: ImmutableMap<string, Relationship>,
|
||||||
@ -113,12 +111,8 @@ function groupSuggestions(
|
|||||||
return 'available';
|
return 'available';
|
||||||
}
|
}
|
||||||
|
|
||||||
const canAccountBeAddedByFollowers =
|
|
||||||
account.feature_approval.automatic.includes('followers') ||
|
|
||||||
account.feature_approval.manual.includes('followers');
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
canAccountBeAddedByFollowers &&
|
canAccountBeAddedByFollowers(account) &&
|
||||||
!relationships.get(account.id)?.following
|
!relationships.get(account.id)?.following
|
||||||
) {
|
) {
|
||||||
return 'mustFollow';
|
return 'mustFollow';
|
||||||
@ -212,7 +206,7 @@ export const CollectionAccounts: React.FC<{
|
|||||||
const [searchValue, setSearchValue] = useState('');
|
const [searchValue, setSearchValue] = useState('');
|
||||||
|
|
||||||
const hasItems = editorItems.length > 0;
|
const hasItems = editorItems.length > 0;
|
||||||
const hasMaxItems = editorItems.length === MAX_ACCOUNT_COUNT;
|
const hasMaxItems = editorItems.length === MAX_COLLECTION_ACCOUNT_COUNT;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
accounts: suggestedAccounts,
|
accounts: suggestedAccounts,
|
||||||
@ -406,7 +400,10 @@ export const CollectionAccounts: React.FC<{
|
|||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='collections.hints.accounts_counter'
|
id='collections.hints.accounts_counter'
|
||||||
defaultMessage='{count}/{max} accounts'
|
defaultMessage='{count}/{max} accounts'
|
||||||
values={{ count: editorItems.length, max: MAX_ACCOUNT_COUNT }}
|
values={{
|
||||||
|
count: editorItems.length,
|
||||||
|
max: MAX_COLLECTION_ACCOUNT_COUNT,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</AccountsHeadingElement>
|
</AccountsHeadingElement>
|
||||||
)}
|
)}
|
||||||
@ -426,7 +423,7 @@ export const CollectionAccounts: React.FC<{
|
|||||||
id='collections.accounts.empty_description'
|
id='collections.accounts.empty_description'
|
||||||
defaultMessage='Add up to {count} accounts'
|
defaultMessage='Add up to {count} accounts'
|
||||||
values={{
|
values={{
|
||||||
count: MAX_ACCOUNT_COUNT,
|
count: MAX_COLLECTION_ACCOUNT_COUNT,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import type { ApiMutedAccountJSON } from '@/mastodon/api_types/accounts';
|
||||||
|
import type { Account } from '@/mastodon/models/account';
|
||||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
export function areCollectionsEnabled() {
|
export function areCollectionsEnabled() {
|
||||||
@ -5,3 +7,12 @@ export function areCollectionsEnabled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
export const getCollectionPath = (id: string) => `/collections/${id}`;
|
||||||
|
|
||||||
|
export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) =>
|
||||||
|
['automatic', 'manual'].includes(account.feature_approval.current_user);
|
||||||
|
|
||||||
|
export const canAccountBeAddedByFollowers = (
|
||||||
|
account: ApiMutedAccountJSON | Account,
|
||||||
|
) =>
|
||||||
|
account.feature_approval.automatic.includes('followers') ||
|
||||||
|
account.feature_approval.manual.includes('followers');
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback, useId } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { isFulfilled } from '@reduxjs/toolkit';
|
import { isFulfilled } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { Toggle } from '@/mastodon/components/form_fields';
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import { fetchLists } from 'mastodon/actions/lists';
|
import { fetchLists } from 'mastodon/actions/lists';
|
||||||
@ -15,7 +16,6 @@ import {
|
|||||||
} from 'mastodon/api/lists';
|
} from 'mastodon/api/lists';
|
||||||
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
import type { ApiListJSON } from 'mastodon/api_types/lists';
|
||||||
import { Button } from 'mastodon/components/button';
|
import { Button } from 'mastodon/components/button';
|
||||||
import { CheckBox } from 'mastodon/components/check_box';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import { IconButton } from 'mastodon/components/icon_button';
|
import { IconButton } from 'mastodon/components/icon_button';
|
||||||
import { getOrderedLists } from 'mastodon/selectors/lists';
|
import { getOrderedLists } from 'mastodon/selectors/lists';
|
||||||
@ -42,6 +42,8 @@ const ListItem: React.FC<{
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
onChange: (id: string, checked: boolean) => void;
|
onChange: (id: string, checked: boolean) => void;
|
||||||
}> = ({ id, title, checked, onChange }) => {
|
}> = ({ id, title, checked, onChange }) => {
|
||||||
|
const uniqueId = useId();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onChange(id, e.target.checked);
|
onChange(id, e.target.checked);
|
||||||
@ -50,14 +52,13 @@ const ListItem: React.FC<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/label-has-associated-control
|
<label className='lists__item' htmlFor={uniqueId}>
|
||||||
<label className='lists__item'>
|
|
||||||
<div className='lists__item__title'>
|
<div className='lists__item__title'>
|
||||||
<Icon id='list-ul' icon={ListAltIcon} />
|
<Icon id='list-ul' icon={ListAltIcon} />
|
||||||
<span>{title}</span>
|
<span>{title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CheckBox value={id} checked={checked} onChange={handleChange} />
|
<Toggle id={uniqueId} checked={checked} onChange={handleChange} />
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -77,6 +77,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'DOMAIN_BLOCK': DomainBlockModal,
|
'DOMAIN_BLOCK': DomainBlockModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'REPORT_COLLECTION': ReportCollectionModal,
|
'REPORT_COLLECTION': ReportCollectionModal,
|
||||||
|
'COLLECTION_ADDER': () => import('@/mastodon/features/collection_adder').then(module => ({ default: module.CollectionAdder })),
|
||||||
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/components/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
'SHARE_COLLECTION': () => import('@/mastodon/features/collections/components/share_modal').then(module => ({ default: module.CollectionShareModal })),
|
||||||
'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
'REVOKE_COLLECTION_INCLUSION': () => import('@/mastodon/features/collections/detail/revoke_collection_inclusion_modal').then(module => ({ default: module.RevokeCollectionInclusionModal })),
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
|
|||||||
@ -86,6 +86,7 @@
|
|||||||
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
"account.locked_info": "This account privacy status is set to locked. The owner manually reviews who can follow them.",
|
||||||
"account.media": "Media",
|
"account.media": "Media",
|
||||||
"account.mention": "Mention @{name}",
|
"account.mention": "Mention @{name}",
|
||||||
|
"account.menu.add_to_collection": "Add to collection…",
|
||||||
"account.menu.add_to_list": "Add to list…",
|
"account.menu.add_to_list": "Add to list…",
|
||||||
"account.menu.block": "Block account",
|
"account.menu.block": "Block account",
|
||||||
"account.menu.block_domain": "Block {domain}",
|
"account.menu.block_domain": "Block {domain}",
|
||||||
@ -372,6 +373,7 @@
|
|||||||
"collections.accounts.empty_description": "Add up to {count} accounts",
|
"collections.accounts.empty_description": "Add up to {count} accounts",
|
||||||
"collections.accounts.empty_editor_title": "No one is in this collection yet",
|
"collections.accounts.empty_editor_title": "No one is in this collection yet",
|
||||||
"collections.accounts.empty_title": "This collection is empty",
|
"collections.accounts.empty_title": "This collection is empty",
|
||||||
|
"collections.add_to_collection": "Add {name} to collections",
|
||||||
"collections.block_collection_owner": "Block account",
|
"collections.block_collection_owner": "Block account",
|
||||||
"collections.by_account": "by {account_handle}",
|
"collections.by_account": "by {account_handle}",
|
||||||
"collections.collection_description": "Description",
|
"collections.collection_description": "Description",
|
||||||
|
|||||||
@ -283,8 +283,12 @@ const collectionSlice = createSlice({
|
|||||||
builder.addCase(addCollectionItem.fulfilled, (state, action) => {
|
builder.addCase(addCollectionItem.fulfilled, (state, action) => {
|
||||||
const { collection_item } = action.payload;
|
const { collection_item } = action.payload;
|
||||||
const { collectionId } = action.meta.arg;
|
const { collectionId } = action.meta.arg;
|
||||||
|
const collection = state.collections[collectionId];
|
||||||
|
|
||||||
state.collections[collectionId]?.items.push(collection_item);
|
if (collection) {
|
||||||
|
collection.items.push(collection_item);
|
||||||
|
collection.item_count++;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -302,6 +306,7 @@ const collectionSlice = createSlice({
|
|||||||
collection.items = collection.items.filter(
|
collection.items = collection.items.filter(
|
||||||
(item) => item.id !== itemId,
|
(item) => item.id !== itemId,
|
||||||
);
|
);
|
||||||
|
collection.item_count--;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user