mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
426 lines
11 KiB
TypeScript
426 lines
11 KiB
TypeScript
import { useCallback, useMemo } from 'react';
|
|
|
|
import { FormattedMessage, useIntl } from 'react-intl';
|
|
|
|
import { useHistory } from 'react-router-dom';
|
|
|
|
import { isFulfilled } from '@reduxjs/toolkit';
|
|
|
|
import { languages } from '@/flavours/glitch/initial_state';
|
|
import {
|
|
hasSpecialCharacters,
|
|
inputToHashtag,
|
|
} from '@/flavours/glitch/utils/hashtags';
|
|
import type {
|
|
ApiCreateCollectionPayload,
|
|
ApiUpdateCollectionPayload,
|
|
} from 'flavours/glitch/api_types/collections';
|
|
import { Button } from 'flavours/glitch/components/button';
|
|
import {
|
|
CheckboxField,
|
|
ComboboxField,
|
|
Fieldset,
|
|
FormStack,
|
|
RadioButtonField,
|
|
SelectField,
|
|
TextAreaField,
|
|
} from 'flavours/glitch/components/form_fields';
|
|
import { TextInputField } from 'flavours/glitch/components/form_fields/text_input_field';
|
|
import { useSearchTags } from 'flavours/glitch/hooks/useSearchTags';
|
|
import type { TagSearchResult } from 'flavours/glitch/hooks/useSearchTags';
|
|
import {
|
|
createCollection,
|
|
updateCollection,
|
|
updateCollectionEditorField,
|
|
} from 'flavours/glitch/reducers/slices/collections';
|
|
import { useAppDispatch, useAppSelector } from 'flavours/glitch/store';
|
|
|
|
import classes from './styles.module.scss';
|
|
import { WizardStepHeader } from './wizard_step_header';
|
|
|
|
export const CollectionDetails: React.FC = () => {
|
|
const dispatch = useAppDispatch();
|
|
const history = useHistory();
|
|
const { id, name, description, topic, discoverable, sensitive, accountIds } =
|
|
useAppSelector((state) => state.collections.editor);
|
|
|
|
const handleNameChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'name',
|
|
value: event.target.value,
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const handleDescriptionChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'description',
|
|
value: event.target.value,
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const handleDiscoverableChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'discoverable',
|
|
value: event.target.value === 'public',
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const handleSensitiveChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'sensitive',
|
|
value: event.target.checked,
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const handleSubmit = useCallback(
|
|
(e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (id) {
|
|
const payload: ApiUpdateCollectionPayload = {
|
|
id,
|
|
name,
|
|
description,
|
|
tag_name: topic || null,
|
|
discoverable,
|
|
sensitive,
|
|
};
|
|
|
|
void dispatch(updateCollection({ payload })).then(() => {
|
|
history.goBack();
|
|
});
|
|
} else {
|
|
const payload: ApiCreateCollectionPayload = {
|
|
name,
|
|
description,
|
|
discoverable,
|
|
sensitive,
|
|
account_ids: accountIds,
|
|
};
|
|
if (topic) {
|
|
payload.tag_name = topic;
|
|
}
|
|
|
|
void dispatch(
|
|
createCollection({
|
|
payload,
|
|
}),
|
|
).then((result) => {
|
|
if (isFulfilled(result)) {
|
|
history.replace(`/collections`);
|
|
history.push(`/collections/${result.payload.collection.id}`, {
|
|
newCollection: true,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
},
|
|
[
|
|
id,
|
|
name,
|
|
description,
|
|
topic,
|
|
discoverable,
|
|
sensitive,
|
|
dispatch,
|
|
history,
|
|
accountIds,
|
|
],
|
|
);
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className={classes.form}>
|
|
<FormStack className={classes.formFieldStack}>
|
|
{!id && (
|
|
<WizardStepHeader
|
|
step={2}
|
|
title={
|
|
<FormattedMessage
|
|
id='collections.create.basic_details_title'
|
|
defaultMessage='Basic details'
|
|
/>
|
|
}
|
|
/>
|
|
)}
|
|
<TextInputField
|
|
required
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.collection_name'
|
|
defaultMessage='Name'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.name_length_hint'
|
|
defaultMessage='40 characters limit'
|
|
/>
|
|
}
|
|
value={name}
|
|
onChange={handleNameChange}
|
|
maxLength={40}
|
|
/>
|
|
|
|
<TextAreaField
|
|
required
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.collection_description'
|
|
defaultMessage='Description'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.description_length_hint'
|
|
defaultMessage='100 characters limit'
|
|
/>
|
|
}
|
|
value={description}
|
|
onChange={handleDescriptionChange}
|
|
maxLength={100}
|
|
/>
|
|
|
|
<TopicField />
|
|
|
|
<LanguageField />
|
|
|
|
<Fieldset
|
|
legend={
|
|
<FormattedMessage
|
|
id='collections.visibility_title'
|
|
defaultMessage='Visibility'
|
|
/>
|
|
}
|
|
>
|
|
<RadioButtonField
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.visibility_public'
|
|
defaultMessage='Public'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.visibility_public_hint'
|
|
defaultMessage='Discoverable in search results and other areas where recommendations appear.'
|
|
/>
|
|
}
|
|
value='public'
|
|
checked={discoverable}
|
|
onChange={handleDiscoverableChange}
|
|
/>
|
|
<RadioButtonField
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.visibility_unlisted'
|
|
defaultMessage='Unlisted'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.visibility_unlisted_hint'
|
|
defaultMessage='Visible to anyone with a link. Hidden from search results and recommendations.'
|
|
/>
|
|
}
|
|
value='unlisted'
|
|
checked={!discoverable}
|
|
onChange={handleDiscoverableChange}
|
|
/>
|
|
</Fieldset>
|
|
|
|
<Fieldset
|
|
legend={
|
|
<FormattedMessage
|
|
id='collections.content_warning'
|
|
defaultMessage='Content warning'
|
|
/>
|
|
}
|
|
>
|
|
<CheckboxField
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.mark_as_sensitive'
|
|
defaultMessage='Mark as sensitive'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.mark_as_sensitive_hint'
|
|
defaultMessage="Hides the collection's description and accounts behind a content warning. The collection name will still be visible."
|
|
/>
|
|
}
|
|
checked={sensitive}
|
|
onChange={handleSensitiveChange}
|
|
/>
|
|
</Fieldset>
|
|
</FormStack>
|
|
|
|
<div className={classes.stickyFooter}>
|
|
<div className={classes.actionWrapper}>
|
|
<Button type='submit'>
|
|
{id ? (
|
|
<FormattedMessage id='lists.save' defaultMessage='Save' />
|
|
) : (
|
|
<FormattedMessage
|
|
id='collections.create_collection'
|
|
defaultMessage='Create collection'
|
|
/>
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const TopicField: React.FC = () => {
|
|
const intl = useIntl();
|
|
const dispatch = useAppDispatch();
|
|
const { topic } = useAppSelector((state) => state.collections.editor);
|
|
|
|
const { tags, isLoading, searchTags } = useSearchTags({
|
|
query: topic,
|
|
});
|
|
|
|
const handleTopicChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'topic',
|
|
value: inputToHashtag(event.target.value),
|
|
}),
|
|
);
|
|
searchTags(event.target.value);
|
|
},
|
|
[dispatch, searchTags],
|
|
);
|
|
|
|
const handleSelectTopicSuggestion = useCallback(
|
|
(item: TagSearchResult) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'topic',
|
|
value: inputToHashtag(item.name),
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
const topicHasSpecialCharacters = useMemo(
|
|
() => hasSpecialCharacters(topic),
|
|
[topic],
|
|
);
|
|
|
|
return (
|
|
<ComboboxField
|
|
required={false}
|
|
icon={null}
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.collection_topic'
|
|
defaultMessage='Topic'
|
|
/>
|
|
}
|
|
hint={
|
|
<FormattedMessage
|
|
id='collections.topic_hint'
|
|
defaultMessage='Add a hashtag that helps others understand the main topic of this collection.'
|
|
/>
|
|
}
|
|
value={topic}
|
|
items={tags}
|
|
isLoading={isLoading}
|
|
renderItem={renderTagItem}
|
|
onSelectItem={handleSelectTopicSuggestion}
|
|
onChange={handleTopicChange}
|
|
autoCapitalize='off'
|
|
autoCorrect='off'
|
|
spellCheck='false'
|
|
maxLength={40}
|
|
status={
|
|
topicHasSpecialCharacters
|
|
? {
|
|
variant: 'warning',
|
|
message: intl.formatMessage({
|
|
id: 'collections.topic_special_chars_hint',
|
|
defaultMessage:
|
|
'Special characters will be removed when saving',
|
|
}),
|
|
}
|
|
: undefined
|
|
}
|
|
suppressMenu={!tags.length}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const renderTagItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
|
|
|
const LanguageField: React.FC = () => {
|
|
const dispatch = useAppDispatch();
|
|
const initialLanguage = useAppSelector(
|
|
(state) => state.compose.get('default_language') as string,
|
|
);
|
|
const { language } = useAppSelector((state) => state.collections.editor);
|
|
|
|
const selectedLanguage = language ?? initialLanguage;
|
|
|
|
const handleLanguageChange = useCallback(
|
|
(event: React.ChangeEvent<HTMLSelectElement>) => {
|
|
dispatch(
|
|
updateCollectionEditorField({
|
|
field: 'language',
|
|
value: event.target.value,
|
|
}),
|
|
);
|
|
},
|
|
[dispatch],
|
|
);
|
|
|
|
return (
|
|
<SelectField
|
|
label={
|
|
<FormattedMessage
|
|
id='collections.collection_language'
|
|
defaultMessage='Language'
|
|
/>
|
|
}
|
|
value={selectedLanguage}
|
|
onChange={handleLanguageChange}
|
|
>
|
|
<option value=''>
|
|
<FormattedMessage
|
|
id='collections.collection_language_none'
|
|
defaultMessage='None'
|
|
/>
|
|
</option>
|
|
{languages?.map(([code, name, localName]) => (
|
|
<option key={code} value={code}>
|
|
{localName} ({name})
|
|
</option>
|
|
))}
|
|
</SelectField>
|
|
);
|
|
};
|