Merge commit '0caf334891e0a5f4e995f1d667eb83d3aad7e41e' into glitch-soc/merge-upstream

Conflicts:
- `lib/mastodon/version.rb`:
  Upstream updated the API version, while we also have a custom glitch API version.
  Updated as upstream did.
This commit is contained in:
Claire committed 2026-05-30 12:13:35 +02:00
commit ae2ff31f32
114 files changed
+1147 -740

No files matched your search

@@ -3,6 +3,7 @@ import { createAction } from '@reduxjs/toolkit';
import {
apiRemoveAccountFromFollowers,
apiGetEndorsedAccounts,
apiGetAccounts,
} from 'mastodon/api/accounts';
import type { ApiRelationshipJSON } from 'mastodon/api_types/relationships';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
@@ -113,3 +114,12 @@ export const fetchEndorsedAccounts = createDataLoadingThunk(
return data;
},
);
export const fetchAccounts = createDataLoadingThunk(
'accounts/multi_accounts',
({ accountIds }: { accountIds: string[] }) => apiGetAccounts(accountIds),
(data, { dispatch }) => {
dispatch(importFetchedAccounts(data));
return data;
},
);
@@ -36,9 +36,18 @@ function notificationTypeForFilter(type: NotificationType) {
}
function notificationTypeForQuickFilter(type: NotificationType) {
if (type === 'quoted_update') return 'update';
else if (type === 'quote') return 'mention';
else return type;
switch (type) {
case 'quoted_update':
return 'update';
case 'quote':
return 'mention';
case 'collection_update':
return 'collection';
case 'added_to_collection':
return 'collection';
default:
return type;
}
}
function excludeAllTypesExcept(filter: string) {
@@ -86,6 +86,9 @@ export function setupBrowserNotifications() {
};
}
/**
* @param {(NotificationPermission) => void} callback
*/
export function requestBrowserPermission(callback = noOp) {
return dispatch => {
requestNotificationPermission((permission) => {
+5
View File
@@ -19,6 +19,11 @@ import type {
ApiProfileUpdateParams,
} from '../api_types/profile';
export const apiGetAccounts = (ids: string[]) =>
apiRequestGet<ApiAccountJSON[]>('v1/accounts', {
id: ids,
});
export const apiSubmitAccountNote = (id: string, value: string) =>
apiRequestPost<ApiRelationshipJSON>(`v1/accounts/${id}/note`, {
comment: value,
+9 -17
View File
@@ -15,48 +15,40 @@ import type {
} from '../api_types/collections';
export const apiCreateCollection = (collection: ApiCreateCollectionPayload) =>
apiRequestPost<ApiWrappedCollectionJSON>('v1_alpha/collections', collection);
apiRequestPost<ApiWrappedCollectionJSON>('v1/collections', collection);
export const apiUpdateCollection = ({
id,
...collection
}: ApiUpdateCollectionPayload) =>
apiRequestPut<ApiWrappedCollectionJSON>(
`v1_alpha/collections/${id}`,
collection,
);
apiRequestPut<ApiWrappedCollectionJSON>(`v1/collections/${id}`, collection);
export const apiDeleteCollection = (collectionId: string) =>
apiRequestDelete(`v1_alpha/collections/${collectionId}`);
apiRequestDelete(`v1/collections/${collectionId}`);
export const apiGetCollection = (collectionId: string) =>
apiRequestGet<ApiCollectionWithAccountsJSON>(
`v1_alpha/collections/${collectionId}`,
`v1/collections/${collectionId}`,
);
export const apiGetCollectionsCreatedByAccount = (accountId: string) =>
apiRequestGet<ApiCollectionsJSON>(
`v1_alpha/accounts/${accountId}/collections`,
);
apiRequestGet<ApiCollectionsJSON>(`v1/accounts/${accountId}/collections`);
export const apiGetCollectionsFeaturingAccount = (accountId: string) =>
apiRequestGet<ApiCollectionsJSON>(
`v1_alpha/accounts/${accountId}/in_collections`,
);
apiRequestGet<ApiCollectionsJSON>(`v1/accounts/${accountId}/in_collections`);
export const apiAddCollectionItem = (collectionId: string, accountId: string) =>
apiRequestPost<WrappedCollectionAccountItem>(
`v1_alpha/collections/${collectionId}/items`,
`v1/collections/${collectionId}/items`,
{ account_id: accountId },
);
export const apiRemoveCollectionItem = (collectionId: string, itemId: string) =>
apiRequestDelete<WrappedCollectionAccountItem>(
`v1_alpha/collections/${collectionId}/items/${itemId}`,
`v1/collections/${collectionId}/items/${itemId}`,
);
export const apiRevokeCollectionInclusion = (
collectionId: string,
itemId: string,
) =>
apiRequestPost(`v1_alpha/collections/${collectionId}/items/${itemId}/revoke`);
) => apiRequestPost(`v1/collections/${collectionId}/items/${itemId}/revoke`);
+1 -1
View File
@@ -15,7 +15,7 @@ export const apiUpdate = (list: Partial<ApiListJSON>) =>
export const apiGetLists = () => apiRequestGet<ApiListJSON[]>('v1/lists');
export const apiGetAccounts = (listId: string) =>
export const apiGetListAccounts = (listId: string) =>
apiRequestGet<ApiAccountJSON[]>(`v1/lists/${listId}/accounts`, {
limit: 0,
});
@@ -110,6 +110,9 @@
word-break: break-all;
text-align: left;
/* Allow the handle text to be selected */
user-select: text;
> svg {
width: 16px;
height: 16px;
@@ -100,6 +100,10 @@ interface ComboboxProps<
* Icon to be displayed in the text input
*/
icon?: TextInputProps['icon'] | null;
/**
* Set to true to open as soon as there is focus
*/
openOnFocus?: boolean;
/**
* Set to false to keep the menu open when an item is selected
*/
@@ -217,8 +221,10 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
renderGroupTitle,
renderItem,
onSelectItem,
onFocus,
onChange,
onKeyDown,
openOnFocus = false,
closeOnSelect = true,
suppressMenu = false,
icon = SearchIcon,
@@ -288,6 +294,16 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
}
}, []);
const handleFocus: React.FocusEventHandler<HTMLInputElement> = useCallback(
(e) => {
if (openOnFocus) {
setShouldMenuOpen(true);
}
onFocus?.(e);
},
[onFocus, openOnFocus],
);
const handleInputChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e);
@@ -487,6 +503,7 @@ const ComboboxWithRef = <Item extends ComboboxItem, GroupKey extends string>(
autoComplete='off'
spellCheck='false'
value={value}
onFocus={handleFocus}
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
icon={icon ?? undefined}
@@ -9,7 +9,6 @@ import { Button } from '@/mastodon/components/button';
import { DisplayName } from '@/mastodon/components/display_name';
import { EmptyState } from '@/mastodon/components/empty_state';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { useAppDispatch } from '@/mastodon/store';
@@ -50,56 +49,39 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
let title: React.ReactNode = null;
let message: React.ReactNode = null;
const hasCollections = areCollectionsEnabled();
if (me === accountId) {
if (hasCollections) {
// Return only here to insert the "Create a collection" button as the action for the empty state.
return (
<EmptyState
title={
// Return only here to insert the "Create a collection" button as the action for the empty state.
return (
<EmptyState
title={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
{!withoutAddCollectionButton && (
<Link to='/collections/new' className='button'>
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts'
defaultMessage='Showcase your favorite accounts'
id='empty_column.account_featured_self.no_collections_button'
defaultMessage='Create a collection'
/>
}
message={
<FormattedMessage
id='empty_column.account_featured_self.showcase_accounts_desc'
defaultMessage='Collections are curated lists of accounts to help others discover more of the Fediverse.'
/>
}
>
{!withoutAddCollectionButton && (
<Link to='/collections/new' className='button'>
<FormattedMessage
id='empty_column.account_featured_self.no_collections_button'
defaultMessage='Create a collection'
/>
</Link>
)}
<Button secondary onClick={confirmHideFeaturedTab}>
<FormattedMessage
id='empty_column.account_featured_self.no_collections_hide_tab'
defaultMessage='Hide this tab instead'
/>
</Button>
</EmptyState>
);
} else {
title = (
<FormattedMessage
id='empty_column.account_featured_self.pre_collections'
defaultMessage='Stay tuned for Collections'
/>
);
message = (
<FormattedMessage
id='empty_column.account_featured_self.pre_collections_desc'
defaultMessage='Collections (coming in Mastodon 4.6) allow you to create your own curated lists of accounts to recommend to others.'
/>
);
}
</Link>
)}
<Button secondary onClick={confirmHideFeaturedTab}>
<FormattedMessage
id='empty_column.account_featured_self.no_collections_hide_tab'
defaultMessage='Hide this tab instead'
/>
</Button>
</EmptyState>
);
} else if (suspended) {
title = (
<FormattedMessage
@@ -30,13 +30,10 @@ import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { CollectionListItem } from '../collections/components/collection_list_item';
import { useCollectionsCreatedBy } from '../collections/overview/created_by_you';
import { areCollectionsEnabled } from '../collections/utils';
import { EmptyMessage } from './components/empty_message';
import { Subheading, SubheadingLink } from './components/subheading';
const collectionsEnabled = areCollectionsEnabled();
const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
multiColumn,
}) => {
@@ -98,14 +95,11 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
);
const hasCollections =
collectionsEnabled &&
collectionsLoadStatus === 'idle' &&
listedCollections.length > 0;
collectionsLoadStatus === 'idle' && listedCollections.length > 0;
const hasFeaturedAccounts = !featuredAccountIds.isEmpty();
const isLoading =
!accountId || (collectionsEnabled && collectionsLoadStatus !== 'idle');
const isLoading = !accountId || collectionsLoadStatus !== 'idle';
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
@@ -165,57 +159,53 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
</ItemList>
</>
)}
{collectionsEnabled && (
<>
<Subheading as='header'>
<h2>
<FormattedMessage
id='account.featured.collections'
defaultMessage='Collections'
/>
</h2>
{accountId === me && (
<SubheadingLink to='/collections/new' icon={AddIcon}>
<FormattedMessage
id='account.featured.new_collection'
defaultMessage='New collection'
/>
</SubheadingLink>
)}
</Subheading>
{hasCollections ? (
<ItemList>
<TruncatedListItems
visibleItems={listedCollections}
truncatedItems={unlistedCollections}
toggleButton={{
title: (
<FormattedMessage
id='collections.unlisted_collections_with_count'
defaultMessage='Unlisted collections ({count})'
values={{ count: unlistedCollections.length }}
/>
),
subtitle: (
<FormattedMessage
id='collections.unlisted_collections_description'
defaultMessage='These don’t appear on your profile to others. Anyone with the link can discover them.'
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
) : (
<EmptyMessage
withoutAddCollectionButton
blockedBy={blockedBy}
hidden={hidden}
suspended={suspended}
accountId={accountId}
<Subheading as='header'>
<h2>
<FormattedMessage
id='account.featured.collections'
defaultMessage='Collections'
/>
</h2>
{accountId === me && (
<SubheadingLink to='/collections/new' icon={AddIcon}>
<FormattedMessage
id='account.featured.new_collection'
defaultMessage='New collection'
/>
)}
</>
</SubheadingLink>
)}
</Subheading>
{hasCollections ? (
<ItemList>
<TruncatedListItems
visibleItems={listedCollections}
truncatedItems={unlistedCollections}
toggleButton={{
title: (
<FormattedMessage
id='collections.unlisted_collections_with_count'
defaultMessage='Unlisted collections ({count})'
values={{ count: unlistedCollections.length }}
/>
),
subtitle: (
<FormattedMessage
id='collections.unlisted_collections_description'
defaultMessage='These don’t appear on your profile to others. Anyone with the link can discover them.'
/>
),
}}
renderListItem={renderListItem}
/>
</ItemList>
) : (
<EmptyMessage
withoutAddCollectionButton
blockedBy={blockedBy}
hidden={hidden}
suspended={suspended}
accountId={accountId}
/>
)}
<RemoteHint accountId={accountId} />
</Scrollable>
@@ -215,6 +215,7 @@ export const CollectionAccounts: React.FC<{
resetAccounts,
} = useSearchAccounts({
withRelationships: true,
withDefaultFollows: searchValue === '',
// Don't suggest accounts that were already added
filterResults: (account) =>
!editorItems.find((item) => item.account_id === account.id),
@@ -363,6 +364,7 @@ export const CollectionAccounts: React.FC<{
)}
{hasPendingItems && <PendingNote />}
<ComboboxField
openOnFocus
id={inputId}
label={intl.formatMessage({
id: 'collections.search_accounts_label',
@@ -23,7 +23,6 @@ import {
userCollectionLimit,
} from '../editor';
import classes from '../styles.module.scss';
import { areCollectionsEnabled } from '../utils';
const CreateButton: React.FC = () => (
<Link to='/collections/new' className='button button--compact'>
@@ -49,7 +48,7 @@ export function useCollectionsCreatedBy(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
if (accountId) {
void dispatch(fetchCollectionsCreatedByAccount({ accountId }));
}
}, [dispatch, accountId]);
@@ -15,7 +15,6 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { CollectionListItem } from '../components/collection_list_item';
import classes from '../styles.module.scss';
import { areCollectionsEnabled } from '../utils';
import { CollectionListError } from './created_by_you';
@@ -23,7 +22,7 @@ function useCollectionsFeaturing(accountId: string | null | undefined) {
const dispatch = useAppDispatch();
useEffect(() => {
if (accountId && areCollectionsEnabled()) {
if (accountId) {
void dispatch(fetchCollectionsFeaturingAccount({ accountId }));
}
}, [dispatch, accountId]);
@@ -1,18 +1,13 @@
import type { ApiMutedAccountJSON } from '@/mastodon/api_types/accounts';
import type { ApiAccountJSON } from '@/mastodon/api_types/accounts';
import type { Account } from '@/mastodon/models/account';
import { isServerFeatureEnabled } from '@/mastodon/utils/environment';
export function areCollectionsEnabled() {
return isServerFeatureEnabled('collections');
}
export const getCollectionPath = (id: string) => `/collections/${id}`;
export const canAccountBeAdded = (account: ApiMutedAccountJSON | Account) =>
export const canAccountBeAdded = (account: ApiAccountJSON | Account) =>
['automatic', 'manual'].includes(account.feature_approval.current_user);
export const canAccountBeAddedByFollowers = (
account: ApiMutedAccountJSON | Account,
account: ApiAccountJSON | Account,
) =>
account.feature_approval.automatic.includes('followers') ||
account.feature_approval.manual.includes('followers');
@@ -15,7 +15,7 @@ import { fetchList } from 'mastodon/actions/lists';
import { openModal } from 'mastodon/actions/modal';
import { apiFollowAccount } from 'mastodon/api/accounts';
import {
apiGetAccounts,
apiGetListAccounts,
apiAddAccountToList,
apiRemoveAccountFromList,
} from 'mastodon/api/lists';
@@ -184,7 +184,7 @@ const ListMembers: React.FC<{
if (id) {
dispatch(fetchList(id));
void apiGetAccounts(id)
void apiGetListAccounts(id)
.then((data) => {
dispatch(importFetchedAccounts(data));
setAccountIds(data.map((a) => a.id));
@@ -12,7 +12,7 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
import { fetchList } from 'mastodon/actions/lists';
import { createList, updateList } from 'mastodon/actions/lists_typed';
import { apiGetAccounts } from 'mastodon/api/lists';
import { apiGetListAccounts } from 'mastodon/api/lists';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import type { RepliesPolicyType } from 'mastodon/api_types/lists';
import { Avatar } from 'mastodon/components/avatar';
@@ -44,7 +44,7 @@ const MembersLink: React.FC<{
const [avatarAccounts, setAvatarAccounts] = useState<ApiAccountJSON[]>([]);
useEffect(() => {
void apiGetAccounts(id)
void apiGetListAccounts(id)
.then((data) => {
setAvatarCount(data.length);
setAvatarAccounts(data.slice(0, 3));
@@ -51,7 +51,6 @@ import { selectUnreadNotificationGroupsCount } from 'mastodon/selectors/notifica
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { AnnualReportNavItem } from '../annual_report/nav_item';
import { areCollectionsEnabled } from '../collections/utils';
import { DisabledAccountBanner } from './components/disabled_account_banner';
import { FollowedTagsPanel } from './components/followed_tags_panel';
@@ -361,18 +360,16 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({
text={intl.formatMessage(messages.bookmarks)}
/>
</li>
{areCollectionsEnabled() && (
<li>
<ColumnLink
transparent
to={`/@${account?.acct}/collections`}
icon='collections'
iconComponent={CollectionsIcon}
activeIconComponent={CollectionsActiveIcon}
text={intl.formatMessage(messages.collections)}
/>
</li>
)}
<li>
<ColumnLink
transparent
to={`/@${account?.acct}/collections`}
icon='collections'
iconComponent={CollectionsIcon}
activeIconComponent={CollectionsActiveIcon}
text={intl.formatMessage(messages.collections)}
/>
</li>
<li>
<ColumnLink
transparent
@@ -12,6 +12,7 @@ import ClearColumnButton from './clear_column_button';
import GrantPermissionButton from './grant_permission_button';
import { PolicyControls } from './policy_controls';
import SettingToggle from './setting_toggle';
import { ColumnSettingsGroup } from './column_settings_group';
class ColumnSettings extends PureComponent {
static propTypes = {
@@ -187,6 +188,11 @@ class ColumnSettings extends PureComponent {
</div>
</section>
<ColumnSettingsGroup
type="collections"
label={<FormattedMessage id='notifications.column_settings.collections' defaultMessage='Collections:' />}
/>
{((this.props.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) && (
<section role='group' aria-labelledby='notifications-admin-sign-up'>
<h3 id='notifications-status'><FormattedMessage id='notifications.column_settings.admin.sign_up' defaultMessage='New sign-ups:' /></h3>
@@ -0,0 +1,162 @@
import { useCallback } from 'react';
import type { FC, ReactNode } from 'react';
import { defineMessages, FormattedMessage } from 'react-intl';
import { showAlert } from '@/mastodon/actions/alerts';
import { requestBrowserPermission } from '@/mastodon/actions/notifications';
import { changeAlerts } from '@/mastodon/actions/push_notifications';
import { changeSetting } from '@/mastodon/actions/settings';
import {
createAppSelector,
useAppDispatch,
useAppSelector,
} from '@/mastodon/store';
import SettingToggle from './setting_toggle';
const selectNotificationSettings = createAppSelector(
[
(state) =>
(state.settings as Immutable.Map<string, unknown>).get(
'notifications',
) as Immutable.Map<string, Immutable.Map<string, unknown> | boolean>,
(state) => state.notifications.get('browserPermission') as string,
(state) => state.push_notifications,
],
(settings, browserPermission, pushSettings) => ({
settings,
browserPermission: browserPermission !== 'denied',
pushSettings,
showPushSettings:
pushSettings.get('supported') && pushSettings.get('enabled'),
}),
);
const messages = defineMessages({
permissionDenied: {
id: 'notifications.permission_denied_alert',
defaultMessage:
"Desktop notifications can't be enabled, as browser permission has been denied before",
},
});
type SettingsType = 'alerts' | 'shows' | 'sounds';
export const ColumnSettingsGroup: FC<{ label: ReactNode; type: string }> = ({
label,
type,
}) => {
const { settings, browserPermission, pushSettings, showPushSettings } =
useAppSelector(selectNotificationSettings);
const dispatch = useAppDispatch();
const handleChange = useCallback(
(path: [SettingsType, string], checked: boolean) => {
if (
path[0] === 'alerts' &&
checked &&
typeof window.Notification !== 'undefined' &&
Notification.permission !== 'granted'
) {
dispatch(
requestBrowserPermission((permission) => {
if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked));
} else {
dispatch(showAlert({ message: messages.permissionDenied }));
}
}),
);
} else {
dispatch(changeSetting(['notifications', ...path], checked));
}
},
[dispatch],
);
const handlePushChange = useCallback(
(path: string[], checked: boolean) => {
if (
checked &&
typeof window.Notification !== 'undefined' &&
Notification.permission !== 'granted'
) {
dispatch(
requestBrowserPermission((permission: NotificationPermission) => {
if (permission === 'granted') {
dispatch(changeAlerts(path, checked));
} else {
dispatch(showAlert({ message: messages.permissionDenied }));
}
}),
);
} else {
dispatch(changeAlerts(path, checked));
}
},
[dispatch],
);
return (
<section role='group' aria-labelledby={`notifications-${type}`}>
<h3 id={`notifications-${type}`}>{label}</h3>
<div className='column-settings__row'>
<SettingToggle
disabled={!browserPermission}
prefix='notifications_desktop'
settings={settings}
settingPath={['alerts', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.alert'
defaultMessage='Desktop notifications'
/>
}
/>
{showPushSettings && (
<SettingToggle
prefix='notifications_push'
settings={pushSettings}
settingPath={['alerts', type]}
onChange={handlePushChange}
label={
<FormattedMessage
id='notifications.column_settings.push'
defaultMessage='Push notifications'
/>
}
/>
)}
<SettingToggle
prefix='notifications'
settings={settings}
settingPath={['shows', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.show'
defaultMessage='Show in column'
/>
}
/>
<SettingToggle
prefix='notifications'
settings={settings}
settingPath={['sounds', type]}
onChange={handleChange}
label={
<FormattedMessage
id='notifications.column_settings.sound'
defaultMessage='Play sound'
/>
}
/>
</div>
</section>
);
};
@@ -3,6 +3,7 @@ import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import CollectionsIcon from '@/material-icons/400-24px/category.svg?react';
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
import PersonAddIcon from '@/material-icons/400-24px/person_add.svg?react';
@@ -26,6 +27,10 @@ const tooltips = defineMessages({
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
collections: {
id: 'notifications.filter.collections',
defaultMessage: 'Collections',
},
statuses: {
id: 'notifications.filter.statuses',
defaultMessage: 'Updates from people you follow',
@@ -124,6 +129,14 @@ export const FilterBar: React.FC = () => {
>
<Icon id='user-plus' icon={PersonAddIcon} />
</BarButton>
<BarButton
selectedFilter={selectedFilter}
type='collection'
key='collection'
title={intl.formatMessage(tooltips.collections)}
>
<Icon id='collections' icon={CollectionsIcon} />
</BarButton>
</div>
);
else
@@ -17,7 +17,6 @@ import { blockAccount } from 'mastodon/actions/accounts';
import { closeModal } from 'mastodon/actions/modal';
import { Button } from 'mastodon/components/button';
import { Icon } from 'mastodon/components/icon';
import { areCollectionsEnabled } from '../../collections/utils';
export const BlockModal = ({ accountId, acct }) => {
const dispatch = useDispatch();
@@ -73,12 +72,10 @@ export const BlockModal = ({ accountId, acct }) => {
<div><FormattedMessage id='block_modal.they_cant_mention' defaultMessage="You can't mention, follow, or quote each other." /></div>
</li>
{areCollectionsEnabled() &&
<li>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CollectionsIcon} /></div>
<div><FormattedMessage id='block_modal.no_collections' defaultMessage="Neither of you can add each other to collections. You'll be automatically removed from each others' existing collections, if applicable." /></div>
</li>
}
<li>
<div className='safety-action-modal__bullet-points__icon'><Icon icon={CollectionsIcon} /></div>
<div><FormattedMessage id='block_modal.no_collections' defaultMessage="Neither of you can add each other to collections. You'll be automatically removed from each others' existing collections, if applicable." /></div>
</li>
</ul>
</div>
@@ -93,7 +93,6 @@ import { CustomHomepage } from 'mastodon/features/custom_homepage';
// Dummy import, to make sure that <Status /> ends up in the application bundle.
// Without this it ends up in ~8 very commonly used bundles.
import '../../components/status';
import { areCollectionsEnabled } from '../collections/utils';
import { getNavigationSkipLinkId, SkipLinks } from './components/skip_links';
const messages = defineMessages({
@@ -235,13 +234,9 @@ class SwitchingColumnsArea extends PureComponent {
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
{areCollectionsEnabled() &&
[
<WrappedRoute path={['/@:acct/collections']} component={Collections} content={children} key='collections-list' />,
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} key='collections-editor' />,
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} key='collections-detail' />,
]
}
<WrappedRoute path={['/@:acct/collections']} component={Collections} content={children} key='collections-list' />
<WrappedRoute path={['/collections/new', '/collections/:id/edit']} component={CollectionsEditor} content={children} key='collections-editor' />
<WrappedRoute path='/collections/:id' component={CollectionDetail} content={children} key='collections-detail' />
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
@@ -8,16 +8,20 @@ import { apiRequest } from 'mastodon/api';
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
import { useAppDispatch } from 'mastodon/store';
import { useCurrentAccountId } from './useAccountId';
export function useSearchAccounts({
onSettled,
filterResults,
resetOnInputClear = true,
withRelationships = false,
withDefaultFollows = false,
}: {
onSettled?: (value: string) => void;
filterResults?: (account: ApiAccountJSON) => boolean;
resetOnInputClear?: boolean;
withRelationships?: boolean;
withDefaultFollows?: boolean;
} = {}) {
const dispatch = useAppDispatch();
@@ -29,7 +33,7 @@ export function useSearchAccounts({
const searchRequestRef = useRef<AbortController | null>(null);
const searchAccounts = useDebouncedCallback(
(value: string) => {
async (value: string) => {
if (searchRequestRef.current) {
searchRequestRef.current.abort();
}
@@ -46,41 +50,100 @@ export function useSearchAccounts({
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));
}
setAccounts(accounts);
setLoadingState('idle');
onSettled?.(value);
})
.catch(() => {
setLoadingState('error');
onSettled?.(value);
});
try {
const data = await apiRequest<ApiAccountJSON[]>(
'GET',
'v1/accounts/search',
{
signal: searchRequestRef.current.signal,
params: {
q: value,
resolve: true,
},
},
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setAccounts(accounts);
setLoadingState('idle');
onSettled?.(value);
} catch {
setLoadingState('error');
onSettled?.(value);
}
},
500,
{ leading: true, trailing: true },
);
const startSearch = useCallback(
(value: string) => {
void searchAccounts(value);
},
[searchAccounts],
);
const resetAccounts = useCallback(() => {
setAccounts([]);
}, []);
return {
searchAccounts,
resetAccounts,
const currentUserId = useCurrentAccountId();
const [defaultAccounts, setDefaultAccounts] = useState<
ApiAccountJSON[] | null
>(null);
useEffect(() => {
if (
!currentUserId ||
loadingState !== 'idle' ||
defaultAccounts !== null ||
!withDefaultFollows
) {
return;
}
async function doRequest() {
setLoadingState('loading');
try {
const data = await apiRequest<ApiAccountJSON[]>(
'GET',
`v1/accounts/${currentUserId}/following`,
{ params: { limit: 40 } },
);
const accounts = filterResults ? data.filter(filterResults) : data;
const accountIds = accounts.map((a) => a.id);
dispatch(importFetchedAccounts(accounts));
if (withRelationships) {
dispatch(fetchRelationships(accountIds));
}
setDefaultAccounts(accounts);
setLoadingState('idle');
} catch {
setLoadingState('error');
}
}
void doRequest();
}, [
currentUserId,
accounts,
dispatch,
filterResults,
loadingState,
withRelationships,
defaultAccounts,
withDefaultFollows,
]);
return {
searchAccounts: startSearch,
resetAccounts,
accounts:
accounts.length === 0 && withDefaultFollows
? (defaultAccounts ?? [])
: accounts,
isLoading: loadingState === 'loading',
isError: loadingState === 'error',
};
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Новыя скаргі:",
"notifications.column_settings.admin.sign_up": "Новыя рэгістрацыі:",
"notifications.column_settings.alert": "Апавяшчэнні на працоўным стале",
"notifications.column_settings.collections": "Калекцыі:",
"notifications.column_settings.favourite": "Упадабанае:",
"notifications.column_settings.filter_bar.advanced": "Паказаць усе катэгорыі",
"notifications.column_settings.filter_bar.category": "Панэль хуткай фільтрацыі",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Праўкі:",
"notifications.filter.all": "Усе",
"notifications.filter.boosts": "Пашырэнні",
"notifications.filter.collections": "Калекцыі",
"notifications.filter.favourites": "Упадабанае",
"notifications.filter.follows": "Падпісаны на",
"notifications.filter.mentions": "Згадванні",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Nye rapporteringer:",
"notifications.column_settings.admin.sign_up": "Nye tilmeldinger:",
"notifications.column_settings.alert": "Computernotifikationer",
"notifications.column_settings.collections": "Samlinger:",
"notifications.column_settings.favourite": "Favoritter:",
"notifications.column_settings.filter_bar.advanced": "Vis alle kategorier",
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Redigeringer:",
"notifications.filter.all": "Alle",
"notifications.filter.boosts": "Fremhævelser",
"notifications.filter.collections": "Samlinger",
"notifications.filter.favourites": "Favoritter",
"notifications.filter.follows": "Følger",
"notifications.filter.mentions": "Omtaler",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Neue Meldungen:",
"notifications.column_settings.admin.sign_up": "Neue Registrierungen:",
"notifications.column_settings.alert": "Desktop-Benachrichtigungen",
"notifications.column_settings.collections": "Sammlungen:",
"notifications.column_settings.favourite": "Favoriten:",
"notifications.column_settings.filter_bar.advanced": "Alle Filterkategorien anzeigen",
"notifications.column_settings.filter_bar.category": "Filterleiste",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Überarbeitete Beiträge:",
"notifications.filter.all": "Alles",
"notifications.filter.boosts": "Geteilte Beiträge",
"notifications.filter.collections": "Sammlungen",
"notifications.filter.favourites": "Favoriten",
"notifications.filter.follows": "Neue Follower",
"notifications.filter.mentions": "Erwähnungen",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Νέες αναφορές:",
"notifications.column_settings.admin.sign_up": "Νέες εγγραφές:",
"notifications.column_settings.alert": "Ειδοποιήσεις για υπολογιστή",
"notifications.column_settings.collections": "Συλλογές:",
"notifications.column_settings.favourite": "Αγαπημένα:",
"notifications.column_settings.filter_bar.advanced": "Εμφάνιση όλων των κατηγοριών",
"notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Επεξεργασίες:",
"notifications.filter.all": "Όλες",
"notifications.filter.boosts": "Προωθήσεις",
"notifications.filter.collections": "Συλλογές",
"notifications.filter.favourites": "Αγαπημένα",
"notifications.filter.follows": "Ακολουθείς",
"notifications.filter.mentions": "Επισημάνσεις",
+2 -2
View File
@@ -637,8 +637,6 @@
"empty_column.account_featured.other": "{acct} has not featured anything yet.",
"empty_column.account_featured_self.no_collections_button": "Create a collection",
"empty_column.account_featured_self.no_collections_hide_tab": "Hide this tab instead",
"empty_column.account_featured_self.pre_collections": "Stay tuned for Collections",
"empty_column.account_featured_self.pre_collections_desc": "Collections (coming in Mastodon 4.6) allow you to create your own curated lists of accounts to recommend to others.",
"empty_column.account_featured_self.showcase_accounts": "Showcase your favorite accounts",
"empty_column.account_featured_self.showcase_accounts_desc": "Collections are curated lists of accounts to help others discover more of the Fediverse.",
"empty_column.account_featured_unknown.other": "This account hasn’t featured anything yet.",
@@ -1000,6 +998,7 @@
"notifications.column_settings.admin.report": "New reports:",
"notifications.column_settings.admin.sign_up": "New sign-ups:",
"notifications.column_settings.alert": "Desktop notifications",
"notifications.column_settings.collections": "Collections:",
"notifications.column_settings.favourite": "Favorites:",
"notifications.column_settings.filter_bar.advanced": "Display all categories",
"notifications.column_settings.filter_bar.category": "Quick filter bar",
@@ -1019,6 +1018,7 @@
"notifications.column_settings.update": "Edits:",
"notifications.filter.all": "All",
"notifications.filter.boosts": "Boosts",
"notifications.filter.collections": "Collections",
"notifications.filter.favourites": "Favorites",
"notifications.filter.follows": "Follows",
"notifications.filter.mentions": "Mentions",
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Nuevas denuncias:",
"notifications.column_settings.admin.sign_up": "Nuevos registros:",
"notifications.column_settings.alert": "Notificaciones de escritorio",
"notifications.column_settings.collections": "Colecciones:",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas las categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Ediciones:",
"notifications.filter.all": "Todas",
"notifications.filter.boosts": "Adhesiones",
"notifications.filter.collections": "Colecciones",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menciones",
@@ -584,6 +584,12 @@
"copy_icon_button.copy_this_text": "Copiar enlace al portapapeles",
"copypaste.copied": "Copiado",
"copypaste.copy_to_clipboard": "Copiar al portapapeles",
"custom_homepage.about": "Acerca de",
"custom_homepage.about_this_server": "Acerca de este servidor",
"custom_homepage.administered_by": "Administrado por",
"custom_homepage.contact": "Contacto:",
"custom_homepage.latest_activity": "Actividad más reciente",
"custom_homepage.these_are_the_latest_posts": "Estas son las últimas 40 publicaciones de las cuentas de este servidor.",
"directory.federated": "Desde el fediverso conocido",
"directory.local": "Sólo de {domain}",
"directory.new_arrivals": "Recién llegados",
+6
View File
@@ -584,6 +584,12 @@
"copy_icon_button.copy_this_text": "Copiar enlace al portapapeles",
"copypaste.copied": "Copiado",
"copypaste.copy_to_clipboard": "Copiar al portapapeles",
"custom_homepage.about": "Acerca de",
"custom_homepage.about_this_server": "Acerca de este servidor",
"custom_homepage.administered_by": "Administrado por",
"custom_homepage.contact": "Contacto:",
"custom_homepage.latest_activity": "Actividad más reciente",
"custom_homepage.these_are_the_latest_posts": "Estas son las últimas 40 publicaciones de las cuentas de este servidor.",
"directory.federated": "Desde el fediverso conocido",
"directory.local": "Solo desde {domain}",
"directory.new_arrivals": "Recién llegados",
+17 -9
View File
@@ -584,6 +584,12 @@
"copy_icon_button.copy_this_text": "Kopeeri link lõikelauale",
"copypaste.copied": "Kopeeritud",
"copypaste.copy_to_clipboard": "Kopeeri vahemällu",
"custom_homepage.about": "Teave",
"custom_homepage.about_this_server": "Teave koduserveri kohta",
"custom_homepage.administered_by": "Seda haldab",
"custom_homepage.contact": "Kontakt:",
"custom_homepage.latest_activity": "Viimane tegevus",
"custom_homepage.these_are_the_latest_posts": "Need on selle serveri kasutajate viimased 40 postitust.",
"directory.federated": "Tuntud födiversumist",
"directory.local": "Ainult domeenilt {domain}",
"directory.new_arrivals": "Uustulijad",
@@ -775,17 +781,17 @@
"home.pending_critical_update.link": "Vaata uuendusi",
"home.pending_critical_update.title": "Saadaval kriitiline turvauuendus!",
"home.show_announcements": "Kuva teadaandeid",
"ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et ignoreerisid nende teavitusi. Teavituste ignoreerimine ei peata sõnumite endi saatmist.",
"ignore_notifications_modal.disclaimer": "Mastodon ei saa teavitada kasutajaid, et eirasid nende teavitusi. Teavituste eiramine ei peata sõnumite endi saatmist.",
"ignore_notifications_modal.filter_instead": "Selle asemel filtreeri",
"ignore_notifications_modal.filter_to_act_users": "Saad endiselt kasutajaid vastu võtta, tagasi lükata või neist teatada",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtreerimine aitab vältida võimalikke segaminiajamisi",
"ignore_notifications_modal.filter_to_review_separately": "Saad filtreeritud teateid eraldi vaadata",
"ignore_notifications_modal.ignore": "Ignoreeri teavitusi",
"ignore_notifications_modal.limited_accounts_title": "Ignoreeri modereeritud kontode teavitusi?",
"ignore_notifications_modal.new_accounts_title": "Ignoreeri uute kontode teavitusi?",
"ignore_notifications_modal.not_followers_title": "Ignoreeri inimeste teavitusi, kes sind ei jälgi?",
"ignore_notifications_modal.not_following_title": "Ignoreeri inimeste teavitusi, keda sa ei jälgi?",
"ignore_notifications_modal.private_mentions_title": "Ignoreeri soovimatute eraviisiliste mainimiste teateid?",
"ignore_notifications_modal.limited_accounts_title": "Kas eirad modereeritud kontode teavitusi?",
"ignore_notifications_modal.new_accounts_title": "Kas eirad uute kontode teavitusi?",
"ignore_notifications_modal.not_followers_title": "Kas eirad teavitusi kasutajatelt, kes sind ei jälgi?",
"ignore_notifications_modal.not_following_title": "Kas eirad teavitusi kasutajatelt, keda sa ei jälgi?",
"ignore_notifications_modal.private_mentions_title": "Kas eirad soovimatute eraviisiliste mainimiste teavitusi?",
"info_button.label": "Abi",
"info_button.what_is_alt_text": "<h1>Mis on alt-tekst?</h1> <p>Alt-tekst pakub pildi kirjeldust nägemispuudega inimeste jaoks või neile, kel on aeglane internet või neile, kes otsivad lisaselgitust</p> <p>Saad parandada ligipääsetavust ja mõistmist kõigi jaoks, kirjutades selge, lühida ja objektiivse alt-teksti.</p> <ul> <li>Lisa tähtsad elemendid</li> <li>Tee pildil olevast tekstist kokkuvõte</li> <li>Kasuta reeglipärast lausestruktuuri</li> <li>Väldi ebaolulist infot</li> <li>Keskendu keerukate vaadete puhul (näiteks diagrammid ja kaardid) puhul trendidele ja põhiseostele</li> </ul>",
"interaction_modal.action": "Suhestumaks kasutaja {name} postitusega palun logi sisse oma Mastodoni kasutajakontoga sõltumata serverist, mida kasutad.",
@@ -879,7 +885,7 @@
"loading_indicator.label": "Laadimine…",
"media_gallery.hide": "Peida",
"moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.",
"mute_modal.hide_from_notifications": "Peida teavituste hulgast",
"mute_modal.hide_from_notifications": "Peida teavituste seast",
"mute_modal.hide_options": "Peida valikud",
"mute_modal.indefinite": "Kuni eemaldan neilt summutamise",
"mute_modal.show_options": "Kuva valikud",
@@ -908,6 +914,7 @@
"navigation_bar.live_feed_local": "Ajajoon reaalajas (sinu server)",
"navigation_bar.live_feed_public": "Ajajoon reaalajas (Födiversum)",
"navigation_bar.logout": "Logi välja",
"navigation_bar.main": "Esileht",
"navigation_bar.moderation": "Modereerimine",
"navigation_bar.more": "Lisavalikud",
"navigation_bar.mutes": "Summutatud kasutajad",
@@ -980,8 +987,8 @@
"notification_requests.dismiss_multiple": "{count, plural, one {Loobuda # taotlusest…} other {Loobuda # taotlusest…}}",
"notification_requests.edit_selection": "Muuda",
"notification_requests.exit_selection": "Valmis",
"notification_requests.explainer_for_limited_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot piiranud.",
"notification_requests.explainer_for_limited_remote_account": "Sellelt kontolt tulevad teavitused on filtreeritud, sest moderaator on seda kontot või serverit piiranud.",
"notification_requests.explainer_for_limited_account": "Selle kasutajakontoga seotud teavitused on filtreeritud, sest moderaator on selle konto tegevust piiranud.",
"notification_requests.explainer_for_limited_remote_account": "Selle kasutajakontoga seotud teavitused on filtreeritud, sest moderaator on selle konto tegevust või suhtlust andtu koduserveriga piiranud.",
"notification_requests.maximize": "Maksimeeri",
"notification_requests.minimize_banner": "Minimeeri filtreeritud teavituste bänner",
"notification_requests.notifications_from": "Teavitus kasutajalt {name}",
@@ -1305,6 +1312,7 @@
"tabs_bar.menu": "Menüü",
"tabs_bar.notifications": "Teated",
"tabs_bar.publish": "Uus postitus",
"tabs_bar.quick_links": "Kiirlingid",
"tabs_bar.search": "Otsing",
"tag.remove": "Eemalda",
"terms_of_service.effective_as_of": "Kehtib alates {date}",
+6
View File
@@ -584,6 +584,12 @@
"copy_icon_button.copy_this_text": "Cóipeáil nasc chuig an ghearrthaisce",
"copypaste.copied": "Cóipeáilte",
"copypaste.copy_to_clipboard": "Cóipeáil chuig an ngearrthaisce",
"custom_homepage.about": "Maidir",
"custom_homepage.about_this_server": "Maidir leis an bhfreastalaí seo",
"custom_homepage.administered_by": "Arna riar ag",
"custom_homepage.contact": "Déan teagmháil le:",
"custom_homepage.latest_activity": "An ghníomhaíocht is déanaí",
"custom_homepage.these_are_the_latest_posts": "Seo iad na 40 postáil is déanaí ó chuntais ar an bhfreastalaí seo.",
"directory.federated": "Ó chomhchruinne aitheanta",
"directory.local": "Ó {domain} amháin",
"directory.new_arrivals": "Daoine atá tar éis teacht",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Novas denuncias:",
"notifications.column_settings.admin.sign_up": "Novas usuarias:",
"notifications.column_settings.alert": "Notificacións de escritorio",
"notifications.column_settings.collections": "Coleccións:",
"notifications.column_settings.favourite": "Favoritas:",
"notifications.column_settings.filter_bar.advanced": "Mostrar todas as categorías",
"notifications.column_settings.filter_bar.category": "Barra de filtrado rápido",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Edicións:",
"notifications.filter.all": "Todo",
"notifications.filter.boosts": "Compartidos",
"notifications.filter.collections": "Coleccións",
"notifications.filter.favourites": "Favoritas",
"notifications.filter.follows": "Seguimentos",
"notifications.filter.mentions": "Mencións",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Nuove segnalazioni:",
"notifications.column_settings.admin.sign_up": "Nuove iscrizioni:",
"notifications.column_settings.alert": "Notifiche desktop",
"notifications.column_settings.collections": "Collezioni:",
"notifications.column_settings.favourite": "Preferiti:",
"notifications.column_settings.filter_bar.advanced": "Mostra tutte le categorie",
"notifications.column_settings.filter_bar.category": "Barra del filtro veloce",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Modifiche:",
"notifications.filter.all": "Tutti",
"notifications.filter.boosts": "Condivisioni",
"notifications.filter.collections": "Collezioni",
"notifications.filter.favourites": "Preferiti",
"notifications.filter.follows": "Seguaci",
"notifications.filter.mentions": "Menzioni",
+2
View File
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Nieuwe rapportages:",
"notifications.column_settings.admin.sign_up": "Nieuwe registraties:",
"notifications.column_settings.alert": "Desktopmeldingen",
"notifications.column_settings.collections": "Verzamelingen:",
"notifications.column_settings.favourite": "Favorieten:",
"notifications.column_settings.filter_bar.advanced": "Alle categorieën tonen",
"notifications.column_settings.filter_bar.category": "Snelle filterbalk",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Bewerkingen:",
"notifications.filter.all": "Alles",
"notifications.filter.boosts": "Boosts",
"notifications.filter.collections": "Verzamelingen",
"notifications.filter.favourites": "Favorieten",
"notifications.filter.follows": "Nieuwe volgers",
"notifications.filter.mentions": "Vermeldingen",
+28 -26
View File
@@ -715,22 +715,22 @@
"follow_suggestions.curated_suggestion": "Escolha da equipe",
"follow_suggestions.dismiss": "Não mostrar novamente",
"follow_suggestions.featured_longer": "Escolhido à mão pela equipe de {domain}",
"follow_suggestions.friends_of_friends_longer": "Popular entre as pessoas que você segue",
"follow_suggestions.hints.featured": "Este perfil foi escolhido a dedo pela equipe {domain}.",
"follow_suggestions.hints.friends_of_friends": "Este perfil é popular entre as pessoas que você segue.",
"follow_suggestions.friends_of_friends_longer": "Em alta entre as pessoas que você segue",
"follow_suggestions.hints.featured": "Este perfil foi escolhido à mão pela equipe de {domain}.",
"follow_suggestions.hints.friends_of_friends": "Este perfil está em alta entre as pessoas que você segue.",
"follow_suggestions.hints.most_followed": "Este perfil é um dos mais seguidos em {domain}.",
"follow_suggestions.hints.most_interactions": "Este perfil tem recebido recentemente muita atenção em {domain}.",
"follow_suggestions.hints.most_interactions": "Este perfil recentemente ganhou bastante atenção em {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "Este perfil é semelhante aos perfis que você seguiu recentemente.",
"follow_suggestions.personalized_suggestion": "Sugestão personalizada",
"follow_suggestions.popular_suggestion": "Sugestão popular",
"follow_suggestions.popular_suggestion_longer": "Popular em {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Similar a perfis que você seguiu recentemente",
"follow_suggestions.view_all": "Visualizar tudo",
"follow_suggestions.popular_suggestion": "Sugestão em alta",
"follow_suggestions.popular_suggestion_longer": "Em alta em {domain}",
"follow_suggestions.similar_to_recently_followed_longer": "Semelhante aos perfis que você seguiu recentemente",
"follow_suggestions.view_all": "Ver tudo",
"follow_suggestions.who_to_follow": "Quem seguir",
"followed_tags": "Hashtags seguidas",
"followers.hide_other_followers": "Este usuário escolheu não deixar visíveis seus seguidores",
"followers.title": "Seguinte {name}",
"following.hide_other_following": "Este usuário escolheu não deixar visíveis aqueles a quem segue",
"followers.hide_other_followers": "O usuário decidiu não deixar seus seguidores visíveis",
"followers.title": "Seguindo {name}",
"following.hide_other_following": "O usuário decidiu não deixar quem eles seguem visível",
"following.title": "Seguido por {name}",
"footer.about": "Sobre",
"footer.about_mastodon": "Sobre o Mastodon",
@@ -740,13 +740,13 @@
"footer.get_app": "Baixe o app",
"footer.keyboard_shortcuts": "Atalhos de teclado",
"footer.privacy_policy": "Política de privacidade",
"footer.source_code": "Exibir código-fonte",
"footer.source_code": "Ver código-fonte",
"footer.status": "Status",
"footer.terms_of_service": "Termos de serviço",
"form_error.blank": "O espaço não pode estar em branco.",
"form_error.blank": "O campo não pode estar vazio.",
"form_field.optional": "(opcional)",
"getting_started.heading": "Primeiros passos",
"hashtag.admin_moderation": "Abrir interface de moderação para #{name}",
"hashtag.admin_moderation": "Abrir menu de moderação para #{name}",
"hashtag.browse": "Buscar publicações em #{hashtag}",
"hashtag.browse_from_account": "Buscar publicações de @{name} em #{hashtag}",
"hashtag.column_header.tag_mode.all": "e {additional}",
@@ -764,27 +764,27 @@
"hashtag.feature": "Destacar no perfil",
"hashtag.follow": "Seguir hashtag",
"hashtag.mute": "Silenciar #{hashtag}",
"hashtag.unfeature": "Não destacar no perfil",
"hashtag.unfeature": "Remover destaque",
"hashtag.unfollow": "Parar de seguir hashtag",
"hashtags.and_other": "…e {count, plural, one {}other {outros #}}",
"hints.profiles.followers_may_be_missing": "Pode haver seguidores deste perfil faltando.",
"hints.profiles.follows_may_be_missing": "Pode haver seguidos por este perfil faltando.",
"hints.profiles.posts_may_be_missing": "É possível que algumas publicações deste perfil estejam faltando.",
"hints.profiles.see_more_followers": "Ver mais seguidores no {domain}",
"hints.profiles.see_more_follows": "Ver mais seguidos no {domain}",
"hashtags.and_other": "…e {count, plural, other {mais #}}",
"hints.profiles.followers_may_be_missing": "Alguns seguidores deste perfil podem estar faltando.",
"hints.profiles.follows_may_be_missing": "Alguns seguidos deste perfil podem estar faltando.",
"hints.profiles.posts_may_be_missing": "Algumas publicações deste perfil podem estar faltando.",
"hints.profiles.see_more_followers": "Ver mais seguidores em {domain}",
"hints.profiles.see_more_follows": "Ver mais perfis seguidos em {domain}",
"hints.profiles.see_more_posts": "Ver mais publicações em {domain}",
"home.column_settings.show_quotes": "Mostrar citações",
"home.column_settings.show_quotes": "Exibir citações",
"home.column_settings.show_reblogs": "Mostrar impulsos",
"home.column_settings.show_replies": "Mostrar respostas",
"home.hide_announcements": "Ocultar anúncios",
"home.pending_critical_update.body": "Por favor, atualize o seu servidor Mastodon o mais rápido possível!",
"home.pending_critical_update.body": "Por favor, atualize o servidor do Mastodon o mais rápido possível!",
"home.pending_critical_update.link": "Ver atualizações",
"home.pending_critical_update.title": "Atualização de segurança crítica disponível!",
"home.show_announcements": "Mostrar anúncios",
"ignore_notifications_modal.disclaimer": "O Mastodon não pode informar aos usuários que você ignorou suas notificações. Ignorar notificações não impedirá que as próprias mensagens sejam enviadas.",
"ignore_notifications_modal.disclaimer": "O Mastodon não informa os usuários se você ignorar as notificações deles. Ignorar notificações não impedirá as mensagens de serem enviadas.",
"ignore_notifications_modal.filter_instead": "Filtrar em vez disso",
"ignore_notifications_modal.filter_to_act_users": "Você ainda conseguirá aceitar, rejeitar ou denunciar usuários",
"ignore_notifications_modal.filter_to_avoid_confusion": "A filtragem ajuda a evitar confusão potencial",
"ignore_notifications_modal.filter_to_act_users": "Você ainda poderá aceitar, rejeitar ou denunciar",
"ignore_notifications_modal.filter_to_avoid_confusion": "Os filtros ajudam a evitar potenciais confusões",
"ignore_notifications_modal.filter_to_review_separately": "Você pode rever notificações filtradas separadamente",
"ignore_notifications_modal.ignore": "Ignorar notificações",
"ignore_notifications_modal.limited_accounts_title": "Ignorar notificações de contas moderadas?",
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "Novas denúncias:",
"notifications.column_settings.admin.sign_up": "Novas inscrições:",
"notifications.column_settings.alert": "Notificações no computador",
"notifications.column_settings.collections": "Coleções:",
"notifications.column_settings.favourite": "Favoritos:",
"notifications.column_settings.filter_bar.advanced": "Exibir todas as categorias",
"notifications.column_settings.filter_bar.category": "Barra de filtro rápido",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "Editar:",
"notifications.filter.all": "Tudo",
"notifications.filter.boosts": "Impulsos",
"notifications.filter.collections": "Coleções",
"notifications.filter.favourites": "Favoritos",
"notifications.filter.follows": "Seguidores",
"notifications.filter.mentions": "Menções",
+2
View File
@@ -999,6 +999,7 @@
"notifications.column_settings.admin.report": "Báo cáo mới:",
"notifications.column_settings.admin.sign_up": "Người mới tham gia:",
"notifications.column_settings.alert": "Báo trên máy tính",
"notifications.column_settings.collections": "Gói khởi đầu:",
"notifications.column_settings.favourite": "Lượt thích:",
"notifications.column_settings.filter_bar.advanced": "Xếp theo từng loại thông báo",
"notifications.column_settings.filter_bar.category": "Phân loại thông báo",
@@ -1018,6 +1019,7 @@
"notifications.column_settings.update": "Sửa tút:",
"notifications.filter.all": "Tất cả",
"notifications.filter.boosts": "Đăng lại",
"notifications.filter.collections": "Gói khởi đầu",
"notifications.filter.favourites": "Lượt thích",
"notifications.filter.follows": "Người theo dõi mới",
"notifications.filter.mentions": "Lượt nhắc đến",
+9 -1
View File
@@ -584,6 +584,12 @@
"copy_icon_button.copy_this_text": "复制链接到剪贴板",
"copypaste.copied": "已复制",
"copypaste.copy_to_clipboard": "复制到剪贴板",
"custom_homepage.about": "关于",
"custom_homepage.about_this_server": "关于此服务器",
"custom_homepage.administered_by": "管理者为",
"custom_homepage.contact": "联系方式:",
"custom_homepage.latest_activity": "最新动态",
"custom_homepage.these_are_the_latest_posts": "以下是来自此服务器账号的最新 40 条嘟文。",
"directory.federated": "来自已知联邦宇宙",
"directory.local": "仅来自 {domain}",
"directory.new_arrivals": "新来者",
@@ -908,7 +914,7 @@
"navigation_bar.live_feed_local": "实时动态(本站)",
"navigation_bar.live_feed_public": "实时动态(公开)",
"navigation_bar.logout": "退出登录",
"navigation_bar.main": "首页",
"navigation_bar.main": "主要",
"navigation_bar.moderation": "审核",
"navigation_bar.more": "更多",
"navigation_bar.mutes": "已隐藏的用户",
@@ -994,6 +1000,7 @@
"notifications.column_settings.admin.report": "新举报:",
"notifications.column_settings.admin.sign_up": "新注册:",
"notifications.column_settings.alert": "桌面通知",
"notifications.column_settings.collections": "收藏列表:",
"notifications.column_settings.favourite": "喜欢:",
"notifications.column_settings.filter_bar.advanced": "显示全部类别",
"notifications.column_settings.filter_bar.category": "快速筛选栏",
@@ -1013,6 +1020,7 @@
"notifications.column_settings.update": "编辑:",
"notifications.filter.all": "全部",
"notifications.filter.boosts": "转嘟",
"notifications.filter.collections": "收藏列表",
"notifications.filter.favourites": "喜欢",
"notifications.filter.follows": "关注",
"notifications.filter.mentions": "提及",
@@ -1000,6 +1000,7 @@
"notifications.column_settings.admin.report": "新檢舉報告:",
"notifications.column_settings.admin.sign_up": "新註冊帳號:",
"notifications.column_settings.alert": "桌面通知",
"notifications.column_settings.collections": "收藏名單:",
"notifications.column_settings.favourite": "最愛:",
"notifications.column_settings.filter_bar.advanced": "顯示所有分類",
"notifications.column_settings.filter_bar.category": "快速過濾器",
@@ -1019,6 +1020,7 @@
"notifications.column_settings.update": "編輯:",
"notifications.filter.all": "全部",
"notifications.filter.boosts": "轉嘟",
"notifications.filter.collections": "收藏名單",
"notifications.filter.favourites": "最愛",
"notifications.filter.follows": "跟隨的使用者",
"notifications.filter.mentions": "提及",
@@ -41,6 +41,7 @@ const initialState = ImmutableMap({
poll: false,
status: false,
update: false,
collections: false,
'admin.sign_up': false,
'admin.report': false,
}),
@@ -65,6 +66,7 @@ const initialState = ImmutableMap({
poll: true,
status: true,
update: true,
collections: true,
'admin.sign_up': true,
'admin.report': true,
}),
@@ -79,6 +81,7 @@ const initialState = ImmutableMap({
poll: true,
status: true,
update: true,
collections: true,
'admin.sign_up': true,
'admin.report': true,
}),
@@ -1,6 +1,7 @@
import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { fetchAccounts } from '@/mastodon/actions/accounts_typed';
import { importFetchedAccounts } from '@/mastodon/actions/importer';
import {
apiCreateCollection,
@@ -20,6 +21,7 @@ import type {
CollectionAccountItem,
} from '@/mastodon/api_types/collections';
import { initialState, me } from '@/mastodon/initial_state';
import type { AppDispatch } from '@/mastodon/store';
import {
createAppAsyncThunk,
createAppSelector,
@@ -322,16 +324,42 @@ const collectionSlice = createSlice({
},
});
/**
* Prefetch accounts whose avatars will be displayed in the collection list
*/
async function importAccountsForPreviewCard(
collections: ApiCollectionJSON[],
dispatch: AppDispatch,
) {
const previewAccountIds = collections
.flatMap((collection) =>
collection.items.slice(0, 3).map((item) => item.account_id),
)
.filter((id): id is string => !!id);
await dispatch(
fetchAccounts({
accountIds: previewAccountIds,
}),
);
}
export const fetchCollectionsCreatedByAccount = createDataLoadingThunk(
`${collectionSlice.name}/fetchCollectionsCreatedByAccount`,
({ accountId }: { accountId: string }) =>
apiGetCollectionsCreatedByAccount(accountId),
async ({ collections }, { dispatch }) => {
await importAccountsForPreviewCard(collections, dispatch);
},
);
export const fetchCollectionsFeaturingAccount = createDataLoadingThunk(
`${collectionSlice.name}/fetchCollectionsFeaturingAccount`,
({ accountId }: { accountId: string }) =>
apiGetCollectionsFeaturingAccount(accountId),
async ({ collections }, { dispatch }) => {
await importAccountsForPreviewCard(collections, dispatch);
},
);
export const fetchCollection = createDataLoadingThunk(
@@ -29,7 +29,10 @@ const filterNotificationsByAllowedTypes = (
(item) =>
item.type === 'gap' ||
allowedType === item.type ||
(allowedType === 'mention' && item.type === 'quote'),
(allowedType === 'mention' && item.type === 'quote') ||
(allowedType === 'collection' &&
(item.type === 'collection_update' ||
item.type === 'added_to_collection')),
);
};
+13 -4
View File
@@ -17,10 +17,19 @@ export const selectSettingsNotificationsShows = createSelector(
export const selectSettingsNotificationsExcludedTypes = createSelector(
[selectSettingsNotificationsShows],
(shows) =>
Object.entries(shows)
.filter(([_type, enabled]) => !enabled)
.map(([type, _enabled]) => type),
(shows) => {
const excludedTypes: string[] = [];
for (const key in shows) {
if (!shows[key]) {
if (key === 'collections') {
excludedTypes.push('collection_update', 'added_to_collection');
} else {
excludedTypes.push(key);
}
}
}
return excludedTypes;
},
);
export const selectSettingsNotificationsQuickFilterShow = (state: RootState) =>
+1 -1
View File
@@ -12,7 +12,7 @@ export function isProduction() {
else return import.meta.env.PROD;
}
export type ServerFeatures = 'fasp' | 'collections';
export type ServerFeatures = 'fasp';
export function isServerFeatureEnabled(feature: ServerFeatures) {
return initialState?.features.includes(feature) ?? false;