Merge commit '4e276e4476c81482143440f1f405479e0e4be7eb' into glitch-soc/merge-upstream
This commit is contained in:
commit
469076c357
118 files changed
+1106
-391
No files matched your search
@@ -1,95 +1,80 @@
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useId, useState } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import type { ApiHashtagJSON } from '@/mastodon/api_types/tags';
|
||||
import { Combobox } from '@/mastodon/components/form_fields';
|
||||
import {
|
||||
addFeaturedTag,
|
||||
clearSearch,
|
||||
updateSearchQuery,
|
||||
} from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||
import { addFeaturedTag } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
type SearchResult = Omit<ApiHashtagJSON, 'url' | 'history'> & {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: {
|
||||
id: 'account_edit_tags.search_placeholder',
|
||||
defaultMessage: 'Enter a hashtag…',
|
||||
},
|
||||
addTag: {
|
||||
id: 'account_edit_tags.add_tag',
|
||||
defaultMessage: 'Add #{tagName}',
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountEditTagSearch: FC = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const {
|
||||
query,
|
||||
tags: suggestedTags,
|
||||
searchTags,
|
||||
resetSearch,
|
||||
isLoading,
|
||||
results: rawResults,
|
||||
} = useAppSelector((state) => state.profileEdit.search);
|
||||
const results = useMemo(() => {
|
||||
if (!rawResults) {
|
||||
return [];
|
||||
}
|
||||
} = useSearchTags({
|
||||
query,
|
||||
// Remove existing featured tags from suggestions
|
||||
filterResults: (tag) => !tag.featuring,
|
||||
});
|
||||
|
||||
const results: SearchResult[] = [...rawResults]; // Make array mutable
|
||||
const trimmedQuery = query.trim();
|
||||
if (
|
||||
trimmedQuery.length > 0 &&
|
||||
results.every(
|
||||
(result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
results.push({
|
||||
id: 'new',
|
||||
name: trimmedQuery,
|
||||
label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}, [intl, query, rawResults]);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSearchChange: ChangeEventHandler<HTMLInputElement> = useCallback(
|
||||
(e) => {
|
||||
void dispatch(updateSearchQuery(e.target.value));
|
||||
setQuery(e.target.value);
|
||||
searchTags(e.target.value);
|
||||
},
|
||||
[dispatch],
|
||||
[searchTags],
|
||||
);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const handleSelect = useCallback(
|
||||
(item: SearchResult) => {
|
||||
void dispatch(clearSearch());
|
||||
(item: TagSearchResult) => {
|
||||
resetSearch();
|
||||
setQuery('');
|
||||
void dispatch(addFeaturedTag({ name: item.name }));
|
||||
},
|
||||
[dispatch],
|
||||
[dispatch, resetSearch],
|
||||
);
|
||||
|
||||
const inputId = useId();
|
||||
const inputLabel = intl.formatMessage(messages.placeholder);
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
items={results}
|
||||
isLoading={isLoading}
|
||||
renderItem={renderItem}
|
||||
onSelectItem={handleSelect}
|
||||
className={classes.autoComplete}
|
||||
icon={SearchIcon}
|
||||
type='search'
|
||||
/>
|
||||
<>
|
||||
<label htmlFor={inputId} className='sr-only'>
|
||||
{inputLabel}
|
||||
</label>
|
||||
<Combobox
|
||||
id={inputId}
|
||||
value={query}
|
||||
onChange={handleSearchChange}
|
||||
placeholder={inputLabel}
|
||||
items={suggestedTags as TagSearchResult[]}
|
||||
isLoading={isLoading}
|
||||
renderItem={renderItem}
|
||||
onSelectItem={handleSelect}
|
||||
className={classes.autoComplete}
|
||||
icon={SearchIcon}
|
||||
type='search'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const renderItem = (item: SearchResult) => item.label ?? `#${item.name}`;
|
||||
const renderItem = (item: TagSearchResult) => item.label ?? `#${item.name}`;
|
||||
@@ -2,10 +2,12 @@ import { useEffect } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useParams } from 'react-router';
|
||||
import { useHistory } from 'react-router';
|
||||
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
|
||||
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
@@ -35,21 +37,27 @@ import { EmptyMessage } from './components/empty_message';
|
||||
import { FeaturedTag } from './components/featured_tag';
|
||||
import type { TagMap } from './components/featured_tag';
|
||||
|
||||
interface Params {
|
||||
acct?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
multiColumn,
|
||||
}) => {
|
||||
const accountId = useAccountId();
|
||||
const account = useAccount(accountId);
|
||||
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
|
||||
const forceEmptyState = suspended || blockedBy || hidden;
|
||||
const { acct = '' } = useParams<Params>();
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const history = useHistory();
|
||||
useEffect(() => {
|
||||
if (
|
||||
account &&
|
||||
!account.show_featured &&
|
||||
isServerFeatureEnabled('profile_redesign')
|
||||
) {
|
||||
history.push(`/@${account.acct}`);
|
||||
}
|
||||
}, [account, history]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId) {
|
||||
void dispatch(fetchFeaturedTags({ accountId }));
|
||||
@@ -166,7 +174,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||
aria-posinset={index + 1}
|
||||
aria-setsize={featuredTags.size}
|
||||
>
|
||||
<FeaturedTag tag={tag} account={acct} />
|
||||
<FeaturedTag tag={tag} account={account?.acct ?? ''} />
|
||||
</Article>
|
||||
))}
|
||||
</ItemList>
|
||||
|
||||
@@ -2,10 +2,9 @@ import { useEffect, useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import { List as ImmutableList, isList } from 'immutable';
|
||||
|
||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
|
||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||
@@ -18,38 +17,69 @@ import Column from 'mastodon/features/ui/components/column';
|
||||
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
||||
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||
import type { RootState } from 'mastodon/store';
|
||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
import {
|
||||
useAppSelector,
|
||||
useAppDispatch,
|
||||
createAppSelector,
|
||||
} from 'mastodon/store';
|
||||
|
||||
import { MediaItem } from './components/media_item';
|
||||
|
||||
const getAccountGallery = createSelector(
|
||||
const emptyList = ImmutableList<MediaAttachment>();
|
||||
|
||||
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
|
||||
|
||||
const selectGalleryTimeline = createAppSelector(
|
||||
[
|
||||
(state: RootState, accountId: string) =>
|
||||
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||
[`account:${accountId}:media`, 'items'],
|
||||
ImmutableList(),
|
||||
) as ImmutableList<string>,
|
||||
(state: RootState) => state.statuses,
|
||||
(_state, accountId?: string | null) => accountId,
|
||||
(state) => state.timelines,
|
||||
(state) => state.accounts,
|
||||
(state) => state.statuses,
|
||||
],
|
||||
(statusIds, statuses) => {
|
||||
let items = ImmutableList<MediaAttachment>();
|
||||
(accountId, timelines, accounts, statuses) => {
|
||||
if (!accountId) {
|
||||
return null;
|
||||
}
|
||||
const account = accounts.get(accountId);
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
statusIds.forEach((statusId) => {
|
||||
const status = statuses.get(statusId) as
|
||||
| ImmutableMap<string, unknown>
|
||||
| undefined;
|
||||
let items = emptyList;
|
||||
const { show_media, show_media_replies } = account;
|
||||
// If the account disabled showing media, don't display anything.
|
||||
if (!show_media && redesignEnabled) {
|
||||
return {
|
||||
items,
|
||||
hasMore: false,
|
||||
isLoading: false,
|
||||
showingReplies: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (status) {
|
||||
const showingReplies = show_media_replies && redesignEnabled;
|
||||
const timeline = timelines.get(
|
||||
`account:${accountId}:media${showingReplies ? ':with_replies' : ''}`,
|
||||
);
|
||||
const statusIds = timeline?.get('items');
|
||||
|
||||
if (isList(statusIds)) {
|
||||
for (const statusId of statusIds) {
|
||||
const status = statuses.get(statusId);
|
||||
items = items.concat(
|
||||
(
|
||||
status.get('media_attachments') as ImmutableList<MediaAttachment>
|
||||
status?.get('media_attachments') as ImmutableList<MediaAttachment>
|
||||
).map((media) => media.set('status', status)),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
return {
|
||||
items,
|
||||
hasMore: !!timeline?.get('hasMore'),
|
||||
isLoading: !!timeline?.get('isLoading'),
|
||||
showingReplies,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@@ -58,27 +88,12 @@ export const AccountGallery: React.FC<{
|
||||
}> = ({ multiColumn }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const accountId = useAccountId();
|
||||
const attachments = useAppSelector((state) =>
|
||||
accountId
|
||||
? getAccountGallery(state, accountId)
|
||||
: ImmutableList<MediaAttachment>(),
|
||||
);
|
||||
const isLoading = useAppSelector((state) =>
|
||||
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||
`account:${accountId}:media`,
|
||||
'isLoading',
|
||||
]),
|
||||
);
|
||||
const hasMore = useAppSelector((state) =>
|
||||
(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||
`account:${accountId}:media`,
|
||||
'hasMore',
|
||||
]),
|
||||
);
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
);
|
||||
const isAccount = !!account;
|
||||
const {
|
||||
isLoading = true,
|
||||
hasMore = false,
|
||||
items: attachments = emptyList,
|
||||
showingReplies: withReplies = false,
|
||||
} = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {};
|
||||
|
||||
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
|
||||
|
||||
@@ -87,16 +102,18 @@ export const AccountGallery: React.FC<{
|
||||
| undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && isAccount) {
|
||||
void dispatch(expandAccountMediaTimeline(accountId));
|
||||
if (accountId) {
|
||||
void dispatch(expandAccountMediaTimeline(accountId, { withReplies }));
|
||||
}
|
||||
}, [dispatch, accountId, isAccount]);
|
||||
}, [dispatch, accountId, withReplies]);
|
||||
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (maxId) {
|
||||
void dispatch(expandAccountMediaTimeline(accountId, { maxId }));
|
||||
void dispatch(
|
||||
expandAccountMediaTimeline(accountId, { maxId, withReplies }),
|
||||
);
|
||||
}
|
||||
}, [dispatch, accountId, maxId]);
|
||||
}, [maxId, dispatch, accountId, withReplies]);
|
||||
|
||||
const handleOpenMedia = useCallback(
|
||||
(attachment: MediaAttachment) => {
|
||||
|
||||
@@ -5,25 +5,16 @@ import { FormattedMessage } from 'react-intl';
|
||||
import type { NavLinkProps } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
|
||||
import { isRedesignEnabled } from '../common';
|
||||
|
||||
import classes from './redesign.module.scss';
|
||||
|
||||
export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
|
||||
if (isRedesignEnabled()) {
|
||||
return (
|
||||
<div className={classes.tabs}>
|
||||
<NavLink isActive={isActive} to={`/@${acct}`}>
|
||||
<FormattedMessage id='account.activity' defaultMessage='Activity' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/@${acct}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
<NavLink exact to={`/@${acct}/featured`}>
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
</NavLink>
|
||||
</div>
|
||||
);
|
||||
return <RedesignTabs />;
|
||||
}
|
||||
return (
|
||||
<div className='account__section-headline'>
|
||||
@@ -49,3 +40,32 @@ export const AccountTabs: FC<{ acct: string }> = ({ acct }) => {
|
||||
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>
|
||||
match?.url === location.pathname ||
|
||||
(!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`));
|
||||
|
||||
const RedesignTabs: FC = () => {
|
||||
const accountId = useAccountId();
|
||||
const account = useAccount(accountId);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { acct, show_featured, show_media } = account;
|
||||
|
||||
return (
|
||||
<div className={classes.tabs}>
|
||||
<NavLink isActive={isActive} to={`/@${acct}`}>
|
||||
<FormattedMessage id='account.activity' defaultMessage='Activity' />
|
||||
</NavLink>
|
||||
{show_media && (
|
||||
<NavLink exact to={`/@${acct}/media`}>
|
||||
<FormattedMessage id='account.media' defaultMessage='Media' />
|
||||
</NavLink>
|
||||
)}
|
||||
{show_featured && (
|
||||
<NavLink exact to={`/@${acct}/featured`}>
|
||||
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||
</NavLink>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
ItemList,
|
||||
Scrollable,
|
||||
} from 'mastodon/components/scrollable_list/components';
|
||||
import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts';
|
||||
import { useAccount } from 'mastodon/hooks/useAccount';
|
||||
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import {
|
||||
addCollectionItem,
|
||||
@@ -374,6 +374,7 @@ export const CollectionAccounts: React.FC<{
|
||||
onSelectItem={
|
||||
isEditMode ? instantToggleAccountItem : toggleAccountItem
|
||||
}
|
||||
closeOnSelect={false}
|
||||
/>
|
||||
{hasMaxAccounts && (
|
||||
<FormattedMessage
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Fragment, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
@@ -6,9 +6,11 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
import { isFulfilled } from '@reduxjs/toolkit';
|
||||
|
||||
import { languages } from '@/mastodon/initial_state';
|
||||
import {
|
||||
hasSpecialCharacters,
|
||||
inputToHashtag,
|
||||
trimHashFromStart,
|
||||
} from '@/mastodon/utils/hashtags';
|
||||
import type {
|
||||
ApiCreateCollectionPayload,
|
||||
@@ -17,12 +19,16 @@ import type {
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import {
|
||||
CheckboxField,
|
||||
ComboboxField,
|
||||
Fieldset,
|
||||
FormStack,
|
||||
RadioButtonField,
|
||||
SelectField,
|
||||
TextAreaField,
|
||||
} from 'mastodon/components/form_fields';
|
||||
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
|
||||
import { useSearchTags } from 'mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from 'mastodon/hooks/useSearchTags';
|
||||
import {
|
||||
createCollection,
|
||||
updateCollection,
|
||||
@@ -34,7 +40,6 @@ import classes from './styles.module.scss';
|
||||
import { WizardStepHeader } from './wizard_step_header';
|
||||
|
||||
export const CollectionDetails: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
const { id, name, description, topic, discoverable, sensitive, accountIds } =
|
||||
@@ -64,18 +69,6 @@ export const CollectionDetails: React.FC = () => {
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleTopicChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
updateCollectionEditorField({
|
||||
field: 'topic',
|
||||
value: inputToHashtag(event.target.value),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[dispatch],
|
||||
);
|
||||
|
||||
const handleDiscoverableChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(
|
||||
@@ -156,11 +149,6 @@ export const CollectionDetails: React.FC = () => {
|
||||
],
|
||||
);
|
||||
|
||||
const topicHasSpecialCharacters = useMemo(
|
||||
() => hasSpecialCharacters(topic),
|
||||
[topic],
|
||||
);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={classes.form}>
|
||||
<FormStack className={classes.formFieldStack}>
|
||||
@@ -213,39 +201,9 @@ export const CollectionDetails: React.FC = () => {
|
||||
maxLength={100}
|
||||
/>
|
||||
|
||||
<TextInputField
|
||||
required={false}
|
||||
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}
|
||||
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
|
||||
}
|
||||
/>
|
||||
<TopicField />
|
||||
|
||||
<LanguageField />
|
||||
|
||||
<Fieldset
|
||||
legend={
|
||||
@@ -335,3 +293,143 @@ export const CollectionDetails: React.FC = () => {
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const TopicField: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { id, topic } = useAppSelector((state) => state.collections.editor);
|
||||
|
||||
const collection = useAppSelector((state) =>
|
||||
id ? state.collections.collections[id] : undefined,
|
||||
);
|
||||
const [isInitialValue, setIsInitialValue] = useState(
|
||||
() => trimHashFromStart(topic) === (collection?.tag?.name ?? ''),
|
||||
);
|
||||
|
||||
const { tags, isLoading, searchTags } = useSearchTags({
|
||||
query: topic,
|
||||
});
|
||||
|
||||
const handleTopicChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsInitialValue(false);
|
||||
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={isInitialValue}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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'
|
||||
tagName={Fragment}
|
||||
/>
|
||||
</option>
|
||||
{languages?.map(([code, name, localName]) => (
|
||||
<option key={code} value={code}>
|
||||
{localName} ({name})
|
||||
</option>
|
||||
))}
|
||||
</SelectField>
|
||||
);
|
||||
};
|
||||
@@ -21,8 +21,7 @@ export const LinkTimeline: React.FC<{
|
||||
const columnRef = useRef<ColumnRef>(null);
|
||||
const firstStatusId = useAppSelector((state) =>
|
||||
decodedUrl
|
||||
? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
|
||||
(state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
|
||||
? (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string)
|
||||
: undefined,
|
||||
);
|
||||
const story = useAppSelector((state) =>
|
||||
|
||||
@@ -28,11 +28,10 @@ import { DisplayName } from 'mastodon/components/display_name';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { ShortNumber } from 'mastodon/components/short_number';
|
||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
import { useSearchAccounts } from './use_search_accounts';
|
||||
|
||||
export const messages = defineMessages({
|
||||
manageMembers: {
|
||||
id: 'column.list_members',
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { fetchRelationships } from 'mastodon/actions/accounts';
|
||||
import { importFetchedAccounts } from 'mastodon/actions/importer';
|
||||
import { apiRequest } from 'mastodon/api';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
export function useSearchAccounts({
|
||||
onSettled,
|
||||
filterResults,
|
||||
resetOnInputClear = true,
|
||||
withRelationships = false,
|
||||
}: {
|
||||
onSettled?: (value: string) => void;
|
||||
filterResults?: (account: ApiAccountJSON) => boolean;
|
||||
resetOnInputClear?: boolean;
|
||||
withRelationships?: boolean;
|
||||
} = {}) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [accountIds, setAccountIds] = useState<string[]>([]);
|
||||
const [loadingState, setLoadingState] = useState<
|
||||
'idle' | 'loading' | 'error'
|
||||
>('idle');
|
||||
|
||||
const searchRequestRef = useRef<AbortController | null>(null);
|
||||
|
||||
const searchAccounts = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
if (searchRequestRef.current) {
|
||||
searchRequestRef.current.abort();
|
||||
}
|
||||
|
||||
if (value.trim().length === 0) {
|
||||
onSettled?.('');
|
||||
if (resetOnInputClear) {
|
||||
setAccountIds([]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingState('loading');
|
||||
|
||||
searchRequestRef.current = new AbortController();
|
||||
|
||||
void apiRequest<ApiAccountJSON[]>('GET', 'v1/accounts/search', {
|
||||
signal: searchRequestRef.current.signal,
|
||||
params: {
|
||||
q: value,
|
||||
resolve: true,
|
||||
},
|
||||
})
|
||||
.then((data) => {
|
||||
const accounts = filterResults ? data.filter(filterResults) : data;
|
||||
const accountIds = accounts.map((a) => a.id);
|
||||
dispatch(importFetchedAccounts(accounts));
|
||||
if (withRelationships) {
|
||||
dispatch(fetchRelationships(accountIds));
|
||||
}
|
||||
setAccountIds(accountIds);
|
||||
setLoadingState('idle');
|
||||
onSettled?.(value);
|
||||
})
|
||||
.catch(() => {
|
||||
setLoadingState('error');
|
||||
onSettled?.(value);
|
||||
});
|
||||
},
|
||||
500,
|
||||
{ leading: true, trailing: true },
|
||||
);
|
||||
|
||||
return {
|
||||
searchAccounts,
|
||||
accountIds,
|
||||
isLoading: loadingState === 'loading',
|
||||
isError: loadingState === 'error',
|
||||
};
|
||||
}
|
||||
@@ -23,7 +23,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
|
||||
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
@@ -182,7 +182,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||
}
|
||||
|
||||
const profileRedesignRoutes = [];
|
||||
if (isClientFeatureEnabled('profile_editing')) {
|
||||
if (isServerFeatureEnabled('profile_redesign')) {
|
||||
profileRedesignRoutes.push(
|
||||
<WrappedRoute key="edit" path='/profile/edit' component={AccountEdit} content={children} />,
|
||||
<WrappedRoute key="featured_tags" path='/profile/featured_tags' component={AccountEditFeaturedTags} content={children} />
|
||||
|
||||
Reference in new issue
Block a user