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 2026-05-30 12:13:35 +02:00
commit ae2ff31f32
114 changed files with 1147 additions and 740 deletions

View File

@ -1,3 +0,0 @@
---
ignore:
- CVE-2026-45363

2
.nvmrc
View File

@ -1 +1 @@
24.15
24.16

View File

@ -135,7 +135,7 @@ group :test do
# Browser integration testing
gem 'capybara', '~> 3.39'
gem 'capybara-playwright-driver'
gem 'playwright-ruby-client', '1.59.1', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
gem 'playwright-ruby-client', '1.60.0', require: false # Pinning the exact version as it needs to be kept in sync with the installed npm package
# Used to reset the database between system tests
gem 'database_cleaner-active_record'
@ -223,7 +223,7 @@ gem 'concurrent-ruby', require: false
gem 'connection_pool', require: false
gem 'xorcist', '~> 1.1'
gem 'net-http', '~> 0.6.0'
gem 'net-http', '~> 0.9.0'
gem 'rubyzip', '~> 3.0'
gem 'hcaptcha', '~> 7.1'

View File

@ -99,8 +99,8 @@ GEM
ast (2.4.3)
attr_required (1.0.2)
aws-eventstream (1.4.0)
aws-partitions (1.1253.0)
aws-sdk-core (3.249.0)
aws-partitions (1.1254.0)
aws-sdk-core (3.250.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
@ -378,7 +378,7 @@ GEM
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
jsonapi-renderer (0.2.2)
jwt (2.10.2)
jwt (2.10.3)
base64
kaminari (1.2.2)
activesupport (>= 4.1.0)
@ -455,8 +455,8 @@ GEM
msgpack (1.8.0)
multi_json (1.20.1)
mutex_m (0.3.0)
net-http (0.6.0)
uri
net-http (0.9.1)
uri (>= 0.11.1)
net-imap (0.6.4)
date
net-protocol
@ -539,7 +539,7 @@ GEM
opentelemetry-instrumentation-active_support (~> 0.10)
opentelemetry-instrumentation-active_support (0.12.0)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-base (0.26.0)
opentelemetry-instrumentation-base (0.26.1)
opentelemetry-api (~> 1.7)
opentelemetry-common (~> 0.21)
opentelemetry-registry (~> 0.1)
@ -559,7 +559,7 @@ GEM
opentelemetry-helpers-sql
opentelemetry-helpers-sql-processor
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rack (0.31.0)
opentelemetry-instrumentation-rack (0.31.1)
opentelemetry-instrumentation-base (~> 0.25)
opentelemetry-instrumentation-rails (0.42.0)
opentelemetry-instrumentation-action_mailer (~> 0.7)
@ -598,7 +598,7 @@ GEM
pg (1.6.3)
pghero (3.8.0)
activerecord (>= 7.2)
playwright-ruby-client (1.59.1)
playwright-ruby-client (1.60.0)
base64
concurrent-ruby (>= 1.1.6)
mime-types (>= 3.0)
@ -816,7 +816,7 @@ GEM
securerandom (0.4.1)
shoulda-matchers (7.0.1)
activesupport (>= 7.1)
sidekiq (8.1.5)
sidekiq (8.1.6)
connection_pool (>= 3.0.0)
json (>= 2.16.0)
logger (>= 1.7.0)
@ -827,7 +827,7 @@ GEM
sidekiq-scheduler (6.0.2)
rufus-scheduler (~> 3.2)
sidekiq (>= 7.3, < 9)
sidekiq-unique-jobs (8.0.13)
sidekiq-unique-jobs (8.1.0)
concurrent-ruby (~> 1.0, >= 1.0.5)
sidekiq (>= 7.0.0, < 9.0.0)
thor (>= 1.0, < 3.0)
@ -1012,7 +1012,7 @@ DEPENDENCIES
memory_profiler
mime-types (~> 3.7.0)
mutex_m
net-http (~> 0.6.0)
net-http (~> 0.9.0)
net-ldap (~> 0.18)
nokogiri (~> 1.15)
omniauth (~> 2.0)
@ -1040,7 +1040,7 @@ DEPENDENCIES
parslet
pg (~> 1.5)
pghero
playwright-ruby-client (= 1.59.1)
playwright-ruby-client (= 1.60.0)
premailer-rails
prometheus_exporter (~> 2.2)
propshaft

View File

@ -9,7 +9,6 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :check_feature_enabled
before_action :require_account_signature!, if: -> { authorized_fetch_mode? }
before_action :set_collections
@ -72,8 +71,4 @@ class ActivityPub::FeaturedCollectionsController < ApplicationController
)
end
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
end

View File

@ -25,6 +25,8 @@ module Admin
@action_logs = @report.history.includes(:target)
@form = Admin::StatusBatchAction.new
@statuses = @report.statuses.with_includes
@collections = @report.collections
@collection_form = Admin::CollectionBatchAction.new
render 'admin/reports/show'
end

View File

@ -1,10 +1,8 @@
# frozen_string_literal: true
class Api::V1Alpha::CollectionItemsController < Api::BaseController
class Api::V1::CollectionItemsController < Api::BaseController
include Authorization
before_action :check_feature_enabled
before_action -> { doorkeeper_authorize! :write, :'write:collections' }
before_action :require_user!
@ -55,8 +53,4 @@ class Api::V1Alpha::CollectionItemsController < Api::BaseController
def set_collection_item
@collection_item = @collection.collection_items.find(params[:id])
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class Api::V1Alpha::CollectionsController < Api::BaseController
class Api::V1::CollectionsController < Api::BaseController
include Authorization
DEFAULT_COLLECTIONS_LIMIT = 40
@ -9,8 +9,6 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
render json: { error: ValidationErrorFormatter.new(e).as_json }, status: 422
end
before_action :check_feature_enabled
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index, :show]
before_action -> { doorkeeper_authorize! :write, :'write:collections' }, only: [:create, :update, :destroy]
@ -91,20 +89,16 @@ class Api::V1Alpha::CollectionsController < Api::BaseController
params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
def next_path
return unless records_continue?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
api_v1_account_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def prev_path
return if offset_param.zero?
api_v1_alpha_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
api_v1_account_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def records_continue?

View File

@ -1,12 +1,10 @@
# frozen_string_literal: true
class Api::V1Alpha::InCollectionsController < Api::BaseController
class Api::V1::InCollectionsController < Api::BaseController
include Authorization
DEFAULT_COLLECTIONS_LIMIT = 40
before_action :check_feature_enabled
before_action -> { authorize_if_got_token! :read, :'read:collections' }, only: [:index]
before_action :require_user!
@ -37,20 +35,16 @@ class Api::V1Alpha::InCollectionsController < Api::BaseController
.limit(limit_param(DEFAULT_COLLECTIONS_LIMIT))
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
def next_path
return unless records_continue?
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param + limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def prev_path
return if offset_param.zero?
api_v1_alpha_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
api_v1_account_in_collections_url(@account, pagination_params(offset: offset_param - limit_param(DEFAULT_COLLECTIONS_LIMIT)))
end
def records_continue?

View File

@ -23,10 +23,6 @@ class Api::V1::ReportsController < Api::BaseController
end
def report_params
if Mastodon::Feature.collections_enabled?
params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: [])
else
params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], rule_ids: [])
end
params.permit(:account_id, :comment, :category, :forward, forward_to_domains: [], status_ids: [], collection_ids: [], rule_ids: [])
end
end

View File

@ -175,6 +175,6 @@ class Api::V1::StatusesController < Api::BaseController
end
def serialized_accounts(accounts)
ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer)
ActiveModel::Serializer::CollectionSerializer.new(accounts, serializer: REST::AccountSerializer, scope_name: :current_user, scope: current_user)
end
end

View File

@ -7,7 +7,6 @@ class CollectionItemsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :check_feature_enabled
before_action :require_account_signature!, if: -> { authorized_fetch_mode? }
before_action :set_collection_item
@ -35,8 +34,4 @@ class CollectionItemsController < ApplicationController
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
not_found
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
end

View File

@ -8,7 +8,6 @@ class CollectionsController < ApplicationController
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }
before_action :check_feature_enabled
before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :set_collection
@ -51,8 +50,4 @@ class CollectionsController < ApplicationController
recently_updated = @collection.updated_at > 15.minutes.ago
recently_updated ? 30.seconds : 5.minutes
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled?
end
end

View File

@ -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;
},
);

View File

@ -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) {

View File

@ -86,6 +86,9 @@ export function setupBrowserNotifications() {
};
}
/**
* @param {(NotificationPermission) => void} callback
*/
export function requestBrowserPermission(callback = noOp) {
return dispatch => {
requestNotificationPermission((permission) => {

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,

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`);

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,
});

View File

@ -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;

View File

@ -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}

View File

@ -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

View File

@ -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 dont 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 dont 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>

View File

@ -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',

View File

@ -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]);

View File

@ -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]);

View File

@ -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');

View File

@ -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));

View File

@ -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));

View File

@ -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

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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

View File

@ -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>

View File

@ -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} />

View File

@ -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',
};

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": "Згадванні",

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",

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",

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": "Επισημάνσεις",

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 hasnt 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",

View File

@ -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",

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": "Sólo de {domain}",
"directory.new_arrivals": "Recién llegados",

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",

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}",

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",

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",

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",

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",

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",

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",

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": "提及",

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": "提及",

View File

@ -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,
}),

View File

@ -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(

View File

@ -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')),
);
};

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) =>

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;

View File

@ -5,7 +5,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity
return accept_follow_for_relay if relay_follow?
return accept_follow!(follow_request_from_object) unless follow_request_from_object.nil?
return accept_quote!(quote_request_from_object) unless quote_request_from_object.nil?
return accept_feature_request! if Mastodon::Feature.collections_enabled? && feature_request_from_object.present?
return accept_feature_request! if feature_request_from_object.present?
case @object['type']
when 'Follow'

View File

@ -13,12 +13,10 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
add_featured
end
when @account.collections_url
return unless Mastodon::Feature.collections_enabled?
add_collection
else
@collection = @account.collections.find_by(uri: value_or_id(@json['target']))
add_collection_item if @collection && Mastodon::Feature.collections_enabled?
add_collection_item if @collection
end
end

View File

@ -3,7 +3,7 @@
class ActivityPub::Activity::Delete < ActivityPub::Activity
def perform
return delete_person if @account.uri == object_uri
return delete_feature_authorization! unless !Mastodon::Feature.collections_enabled? || feature_authorization_from_object.nil?
return delete_feature_authorization! unless feature_authorization_from_object.nil?
delete_object
end

View File

@ -4,7 +4,6 @@ class ActivityPub::Activity::FeatureRequest < ActivityPub::Activity
include Payloadable
def perform
return unless Mastodon::Feature.collections_enabled?
return if non_matching_uri_hosts?(@account.uri, @json['id'])
@collection = find_or_fetch_collection

View File

@ -13,7 +13,7 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
update_account
elsif supported_object_type? || converted_object_type?
update_status
elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_enabled?
elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection'])
update_collection
end
end

View File

@ -5,42 +5,45 @@ class StatusCacheHydrator
@status = status
end
def hydrate(account_id, nested: false)
def hydrate(account_or_id, nested: false)
account = account_or_id.is_a?(Account) ? account_or_id : Account.find(account_or_id)
# The cache of the serialized hash is generated by the fan-out-on-write service
payload = Rails.cache.fetch("fan-out/#{@status.id}") { InlineRenderer.render(@status, nil, :status) }
# If we're delivering to the author who disabled the display of the application used to create the
# status, we need to hydrate the application, since it was not rendered for the basic payload
payload[:application] = payload_application if payload[:application].nil? && @status.account_id == account_id
payload[:application] = payload_application if payload[:application].nil? && @status.account_id == account.id
# We take advantage of the fact that some relationships can only occur with an original status, not
# the reblog that wraps it, so we can assume that some values are always false
if payload[:reblog]
hydrate_reblog_payload(payload, account_id, nested:)
hydrate_reblog_payload(payload, account, nested:)
else
hydrate_non_reblog_payload(payload, account_id, nested:)
hydrate_non_reblog_payload(payload, account, nested:)
end
end
private
def hydrate_non_reblog_payload(empty_payload, account_id, nested: false)
def hydrate_non_reblog_payload(empty_payload, account, nested: false)
empty_payload.tap do |payload|
fill_status_payload(payload, @status, account_id, fresh: !nested, nested:)
fill_status_payload(payload, @status, account, fresh: !nested, nested:)
end
end
def hydrate_reblog_payload(empty_payload, account_id, nested: false)
def hydrate_reblog_payload(empty_payload, account, nested: false)
empty_payload.tap do |payload|
payload[:muted] = false
payload[:bookmarked] = false
payload[:pinned] = false if @status.account_id == account_id
payload[:pinned] = false if @status.account_id == account.id
# If the reblogged status is being delivered to the author who disabled the display of the application
# used to create the status, we need to hydrate it here too
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account.id
fill_status_payload(payload[:reblog], @status.reblog, account_id, fresh: false, nested:)
hydrate_account(payload[:account], account)
fill_status_payload(payload[:reblog], @status.reblog, account, fresh: false, nested:)
payload[:filtered] = payload[:reblog][:filtered]
payload[:favourited] = payload[:reblog][:favourited]
@ -49,36 +52,37 @@ class StatusCacheHydrator
end
end
def fill_status_payload(payload, status, account_id, nested: false, fresh: true)
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id)
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
# TODO: performance optimization by not loading `Account` twice
payload[:quote_approval][:current_user] = status.quote_policy_for_account(Account.find_by(id: account_id)) if payload[:quote_approval]
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account_id, nested:) if payload[:quote]
def fill_status_payload(payload, status, account, nested: false, fresh: true)
payload[:favourited] = Favourite.exists?(account_id: account.id, status_id: status.id)
payload[:reblogged] = Status.exists?(account_id: account.id, reblog_of_id: status.id)
payload[:muted] = ConversationMute.exists?(account_id: account.id, conversation_id: status.conversation_id)
payload[:bookmarked] = Bookmark.exists?(account_id: account.id, status_id: status.id)
payload[:pinned] = StatusPin.exists?(account_id: account.id, status_id: status.id) if status.account_id == account.id
payload[:filtered] = mapped_applied_custom_filter(account, status)
payload[:quote_approval][:current_user] = status.quote_policy_for_account(account) if payload[:quote_approval]
payload[:quote] = hydrate_quote_payload(payload[:quote], status.quote, account, nested:) if payload[:quote]
if payload[:poll]
if fresh
# If the status is brand new, we don't need to look up votes in database
payload[:poll][:voted] = status.account_id == account_id
payload[:poll][:voted] = status.account_id == account.id
payload[:poll][:own_votes] = []
elsif status.account_id == account_id
elsif status.account_id == account.id
payload[:poll][:voted] = true
payload[:poll][:own_votes] = []
else
own_votes = PollVote.where(poll_id: status.poll_id, account_id: account_id).pluck(:choice)
own_votes = PollVote.where(poll_id: status.poll_id, account_id: account.id).pluck(:choice)
payload[:poll][:voted] = !own_votes.empty?
payload[:poll][:own_votes] = own_votes
end
end
payload[:card][:missing_attribution] = status.preview_card.unverified_author_account_id == account_id if payload[:card]
payload[:card][:missing_attribution] = status.preview_card.unverified_author_account_id == account.id if payload[:card]
# Nested statuses are more likely to have a stale cache
fill_status_stats(payload, status) if nested
hydrate_account(payload[:account], account)
end
def fill_status_stats(payload, status)
@ -88,7 +92,7 @@ class StatusCacheHydrator
payload[:quotes_count] = status.quotes_count
end
def hydrate_quote_payload(empty_payload, quote, account_id, nested: false)
def hydrate_quote_payload(empty_payload, quote, account, nested: false)
return unless quote&.acceptable?
empty_payload.tap do |payload|
@ -100,14 +104,14 @@ class StatusCacheHydrator
payload[nested ? :quoted_status_id : :quoted_status] = nil
payload[:state] = 'deleted'
else
filter_state = StatusFilter.new(quote.quoted_status, Account.find_by(id: account_id)).filter_state_for_quote
filter_state = StatusFilter.new(quote.quoted_status, account).filter_state_for_quote
payload[:state] = filter_state || 'accepted'
if filter_state == 'unauthorized'
payload[nested ? :quoted_status_id : :quoted_status] = nil
elsif nested
payload[:quoted_status_id] = quote.quoted_status_id&.to_s
else
payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account_id, nested: true)
payload[:quoted_status] = StatusCacheHydrator.new(quote.quoted_status).hydrate(account, nested: true)
end
end
else
@ -116,9 +120,18 @@ class StatusCacheHydrator
end
end
def mapped_applied_custom_filter(account_id, status)
def hydrate_account(payload, account)
return unless payload[:id]
stale_account = Account.find_by(id: payload[:id])
return if stale_account.nil?
payload[:feature_approval][:current_user] = stale_account.feature_policy_for_account(account)
end
def mapped_applied_custom_filter(account, status)
CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status)
.apply_cached_filters(CustomFilter.cached_filters_for(account), status)
.map { |filter| serialized_filter(filter) }
end

View File

@ -34,9 +34,13 @@ class AdminMailer < ApplicationMailer
end
def new_trends(links, tags, statuses)
@links = links
@tags = tags
@statuses = statuses
ActiveRecord::Associations::Preloader.new(records: [*links, *tags, *statuses], associations: [:trend]).call
@links = links.filter { |link| link.trend.present? }
@tags = tags.filter { |tag| tag.trend.present? }
@statuses = statuses.filter { |status| status.trend.present? }
return unless @links.any? || @tags.any? || @statuses.any?
mail subject: default_i18n_subject(instance: @instance)
end

View File

@ -19,17 +19,15 @@ class NotificationMailer < ApplicationMailer
default to: -> { email_address_with_name(@user.email, @me.username) }
rescue_from(ActiveRecord::RecordNotFound) { false }
layout 'mailer'
def mention
return if @status.blank?
mail subject: default_i18n_subject(name: @status.account.acct)
end
def quote
return if @status.blank?
mail subject: default_i18n_subject(name: @status.account.acct)
end
@ -38,14 +36,10 @@ class NotificationMailer < ApplicationMailer
end
def favourite
return if @status.blank?
mail subject: default_i18n_subject(name: @account.acct)
end
def reblog
return if @status.blank?
mail subject: default_i18n_subject(name: @account.acct)
end
@ -64,7 +58,7 @@ class NotificationMailer < ApplicationMailer
end
def set_status
@status = @notification.target_status
@status = @notification.target_status || raise(ActiveRecord::RecordNotFound)
end
def set_account

View File

@ -10,7 +10,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
:moved_to, :property_value, :discoverable, :suspended,
:memorial, :indexable, :attribution_domains, :profile_settings
context_extensions :interaction_policies if Mastodon::Feature.collections_enabled?
context_extensions :interaction_policies
attributes :id, :webfinger, :type, :following, :followers,
:inbox, :outbox, :featured, :featured_tags,
@ -21,8 +21,8 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
attribute :show_media_replies, key: :show_replies_in_media
attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? }
attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? }
attribute :interaction_policy
attribute :featured_collections
has_one :public_key, serializer: ActivityPub::PublicKeySerializer

View File

@ -21,7 +21,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
attribute :memorial, if: :memorial?
attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? }
attribute :feature_approval
attribute :email_subscriptions, if: -> { Rails.application.config.x.email_subscriptions && Setting.email_subscriptions }
class AccountDecorator < SimpleDelegator

View File

@ -3,7 +3,7 @@
class REST::RoleSerializer < ActiveModel::Serializer
attributes :id, :name, :permissions, :color, :highlighted
attribute :collection_limit, if: -> { Mastodon::Feature.collections_enabled? }
attribute :collection_limit
def id
object.id.to_s

View File

@ -72,7 +72,7 @@ class ActivityPub::ProcessAccountService < BaseService
unless @options[:only_key] || @account.suspended?
check_featured_collection! if @json['featured'].present?
check_featured_tags_collection! if @json['featuredTags'].present?
check_featured_collections_collection! if @json['featuredCollections'].present? && Mastodon::Feature.collections_enabled?
check_featured_collections_collection! if @json['featuredCollections'].present?
check_links! if @account.fields.any?(&:requires_verification?)
end
@ -121,7 +121,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.uri = @uri
@account.actor_type = actor_type
@account.created_at = @json['published'] if @json['published'].present?
@account.feature_approval_policy = feature_approval_policy if Mastodon::Feature.collections_enabled?
@account.feature_approval_policy = feature_approval_policy
end
def valid_collection_uri(uri)

View File

@ -28,7 +28,7 @@ class ResolveURLService < BaseService
status = FetchRemoteStatusService.new.call(resource_url, prefetched_body: body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
elsif type == 'FeaturedCollection' && Mastodon::Feature.collections_enabled?
elsif type == 'FeaturedCollection'
collection = ActivityPub::FetchRemoteFeaturedCollectionService.new.call(resource_url, prefetched_body: body)
authorize_with @on_behalf_of, collection, :show? unless collection.nil?
collection

View File

@ -67,10 +67,9 @@
= material_symbol('photo_camera')
= report.media_attachments_count
- if Mastodon::Feature.collections_enabled?
%span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') }
= material_symbol('category')
= report.collections.size
%span.report-card__summary__item__content__icon{ title: t('admin.accounts.collections') }
= material_symbol('category')
= report.collections.size
- if report.forwarded?
·

View File

@ -62,27 +62,26 @@
- else
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
- if Mastodon::Feature.collections_enabled?
%details{ open: @collections.any? }
%summary
= t 'admin.reports.collections', count: @collections.size
%details{ open: @collections.any? }
%summary
= t 'admin.reports.collections', count: @collections.size
= form_with model: @collection_form, url: batch_admin_account_collections_path(@report.target_account_id, report_id: @report.id) do |f|
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]),
admin_account_collections_path(@report.target_account_id, report_id: @report.id),
class: 'table-action-link'
- if !@collections.empty? && @report.unresolved?
= f.button safe_join([material_symbol('close'), t('admin.collections.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
.batch-table__body
- if @collections.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/shared/collection_batch_row', collection: @collections, as: :collection, locals: { f: f }
= form_with model: @collection_form, url: batch_admin_account_collections_path(@report.target_account_id, report_id: @report.id) do |f|
.batch-table
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= link_to safe_join([material_symbol('add'), t('admin.reports.add_to_report')]),
admin_account_collections_path(@report.target_account_id, report_id: @report.id),
class: 'table-action-link'
- if !@collections.empty? && @report.unresolved?
= f.button safe_join([material_symbol('close'), t('admin.collections.batch.remove_from_report')]), name: :remove_from_report, class: 'table-action-link', type: :submit
.batch-table__body
- if @collections.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'admin/shared/collection_batch_row', collection: @collections, as: :collection, locals: { f: f }
- if @report.unresolved?
%hr.spacer/

View File

@ -32,13 +32,11 @@
%hr.spacer/
- if Mastodon::Feature.collections_enabled?
.fields-group
= form.input :collection_limit,
wrapper: :with_label
.fields-group
= form.input :collection_limit,
wrapper: :with_label
%hr.spacer/
%hr.spacer/
- unless current_user.role == form.object

View File

@ -956,7 +956,14 @@ de:
disabled: Bestimmte Rolle erforderlich
public: Alle
landing_page:
hints:
about_html: Eine Seite mit der Beschreibung, den Kontaktinformationen, den Regeln und weiteren Informationen zu diesem Server.
local_feed_html: Ein Live-Feed mit den neuesten Beiträgen der Nutzer auf diesem Server.
overview_html: Eine Seite, die die Beschreibung deines Servers zusammen mit den neuesten lokalen Beiträgen der Nutzer zeigt.
trends_html: Eine Seite mit dem, was auf diesem Server gerade im Trend liegt.
values:
about: Über-Seite
local_feed: Lokaler Live-Feed
overview: Übersicht
trends: Trendet
registrations:

View File

@ -955,6 +955,17 @@ es-MX:
authenticated: Solo usuarios registrados
disabled: Requerir un rol de usuario específico
public: Todos
landing_page:
hints:
about_html: Una página con la descripción, la información de contacto, las normas y otros datos sobre este servidor.
local_feed_html: Una cronología en tiempo real con las publicaciones más recientes de los usuarios de este servidor.
overview_html: Una página que muestra la descripción de tu servidor junto con las publicaciones locales más recientes de los usuarios de este servidor.
trends_html: Una página que muestra lo que es popular en este servidor en este momento.
values:
about: Página sobre el servidor
local_feed: Cronología local
overview: Resumen
trends: En tendencia
registrations:
moderation_recommandation: "¡Por favor, asegúrate de contar con un equipo de moderación adecuado y activo antes de abrir el registro al público!"
preamble: Controla quién puede crear una cuenta en tu servidor.

View File

@ -955,6 +955,17 @@ es:
authenticated: Solo usuarios autenticados
disabled: Requerir un rol de usuario específico
public: Todos
landing_page:
hints:
about_html: Una página con la descripción, información de contacto, reglas y otra información relevante de este servidor.
local_feed_html: Una cronología con los mensajes más recientes de los usuarios en este servidor.
overview_html: Una página que muestra la descripción de tu servidor junto a las publicaciones locales más recientes de los usuarios en este servidor.
trends_html: Una página destacando lo que es popular en este servidor en este momento.
values:
about: Página sobre el servidor
local_feed: Cronología local
overview: Resumen
trends: En tendencia
registrations:
moderation_recommandation: Por favor, ¡asegúrate de tener un equipo de moderación adecuado y reactivo antes de abrir los registros a todo el mundo!
preamble: Controla quién puede crear una cuenta en tu servidor.

View File

@ -279,8 +279,8 @@ et:
destroy_unavailable_domain_html: "%{name} taastas edastamise domeeni %{target}"
destroy_user_role_html: "%{name} kustutas %{target} rolli"
destroy_username_block_html: "%{name} eemaldas kasutajanime reegli, milles sisaldub %{target}"
disable_2fa_user_html: "%{name} eemaldas kasutaja %{target} kahe etapise nõude"
disable_custom_emoji_html: "%{name} keelas emotikooni %{target}"
disable_2fa_user_html: "%{name} eemaldas kasutajalt %{target} kahefaktorilise autentimise nõude"
disable_custom_emoji_html: "%{name} keelas emoji %{target}"
disable_relay_html: "%{name} eemaldas sõnumivahendusserveri kasutuselt: %{target}"
disable_sign_in_token_auth_user_html: "%{name} keelas e-posti võtme abil autentimise %{target} jaoks"
disable_user_html: "%{name} keelas %{target} sisenemise"
@ -295,7 +295,7 @@ et:
reject_user_html: "%{name} lükkas %{target} liitumissoovi tagasi"
remove_avatar_user_html: "%{name} eemaldas %{target} avatari"
reopen_report_html: "%{name} taasavas raporti %{target}"
resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti"
resend_user_html: "%{name} saatis kasutajale %{target} e-postiga uue kinnituskirja"
reset_password_user_html: "%{name} lähtestas %{target} kasutaja salasõna"
resolve_report_html: "%{name} lahendas raporti %{target}"
sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu"
@ -955,6 +955,17 @@ et:
authenticated: Vaid autenditud kasutajad
disabled: Eelda konkreetse kasutajarolli olemasolu
public: Kõik
landing_page:
hints:
about_html: Leht kirjeldusega, kontaktiteabega, reeglitega muu olulisega selle koduserveri kohta.
local_feed_html: Sisuvoog viimaste selle serveri kasutajate postitustega.
overview_html: Leht, kus näidatakse selle serveri kirjeldust koos serveri kasutate viimaste postitustega.
trends_html: Leht, mis näitab selles serveris hetkel populaarseid teemasid.
values:
about: Serveri teabe leht
local_feed: Kohalik postituste voog
overview: Ülevaade
trends: Populaarsust koguv
registrations:
moderation_recommandation: Enne kõigi jaoks registreerimise avamist veendu, et oleks olemas adekvaatne ja reageerimisvalmis modereerijaskond!
preamble: Kes saab serveril konto luua.

View File

@ -1003,6 +1003,17 @@ ga:
authenticated: Úsáideoirí fíordheimhnithe amháin
disabled: Éiligh ról úsáideora sonrach
public: Gach duine
landing_page:
hints:
about_html: Leathanach leis an gcur síos, eolas teagmhála, rialacha agus faisnéis eile maidir leis an bhfreastalaí seo.
local_feed_html: Fotha beo ina bhfuil na postálacha is déanaí ó úsáideoirí ar an bhfreastalaí seo.
overview_html: Leathanach a thaispeánann cur síos ar do fhreastalaí in éineacht leis na postálacha áitiúla is déanaí ó úsáideoirí ar an bhfreastalaí seo.
trends_html: Leathanach ar a bhfuil an-tóir ar an bhfreastalaí seo faoi láthair.
values:
about: Maidir le leathanach
local_feed: Beatha beo áitiúil
overview: Forbhreathnú
trends: Treocht
registrations:
moderation_recommandation: Cinntigh le do thoil go bhfuil foireann mhodhnóireachta imoibríoch leordhóthanach agat sula n-osclaíonn tú clárúcháin do gach duine!
preamble: Rialú cé atá in ann cuntas a chruthú ar do fhreastalaí.

View File

@ -961,6 +961,9 @@ lt:
your_appeal_rejected: Tavo apeliacija buvo atmesta
edit_profile:
other: Kita
email_subscription_mailer:
confirmation:
action: Patvirtinkite el. pašto adresą
emoji_styles:
auto: Automatinis
native: Vietiniai

View File

@ -148,6 +148,7 @@ lt:
avatar: Profilio nuotrauka
bot: Tai automatinė paskyra
chosen_languages: Filtruoti kalbas
confirm_password: Patvirtink slaptažodį
display_name: Rodomas vardas
email: El. pašto adresas
expires_in: Nustoja galioti po
@ -261,7 +262,9 @@ lt:
jurisdiction: Teisinis teismingumas
min_age: Mažiausias amžius
user:
date_of_birth_1i: Metai
date_of_birth_2i: Mėnuo
date_of_birth_3i: Diena
role: Vaidmuo
time_zone: Laiko juosta
user_role:

View File

@ -956,9 +956,16 @@ tr:
disabled: Belirli kullanıcı rolü gerekir
public: Herkes
landing_page:
hints:
about_html: Bu sunucuya ilişkin açıklama, iletişim bilgileri, kurallar ve diğer bilgilerin yer aldığı bir sayfa.
local_feed_html: Bu sunucudaki kullanıcıların en son paylaşımlarını içeren bir canlı akış.
overview_html: Sunucunuzun açıklamasını ve bu sunucudaki kullanıcıların en son yerel gönderilerini gösteren bir sayfa.
trends_html: Bu sunucuda şu anda popüler olanları gösteren bir sayfa.
values:
about: Hakkında sayfası
local_feed: Yerel canlı akış
overview: Genel Bakış
trends: Öne çıkanlar
registrations:
moderation_recommandation: Lütfen kayıtları herkese açmadan önce yeterli ve duyarlı bir denetleyici ekibine sahip olduğunuzdan emin olun!
preamble: Sunucunuzda kimin hesap oluşturabileceğini denetleyin.

View File

@ -939,6 +939,17 @@ zh-CN:
authenticated: 仅已登录用户
disabled: 需要特定的用户角色
public: 每个人
landing_page:
hints:
about_html: 关于此服务器的描述、联系信息、规则及其他信息的页面。
local_feed_html: 展示此服务器用户最新嘟文的实时动态。
overview_html: 展示你的服务器描述及本站用户最新嘟文的页面。
trends_html: 展示此服务器当前热门内容的页面。
values:
about: 关于本站
local_feed: 本站实时动态
overview: 概览
trends: 当前热门
registrations:
moderation_recommandation: 在向每个人开放注册之前,请确保你拥有一个人手足够且反应迅速的管理团队!
preamble: 控制谁可以在你的服务器上创建账号。

View File

@ -6,13 +6,16 @@ namespace :api, format: false do
# Experimental JSON / REST API
namespace :v1_alpha do
resources :async_refreshes, only: :show
end
# TODO: Remove once apps switch over to v1
scope :v1_alpha, as: :v1_alpha, module: :v1 do
resources :accounts, only: [] do
resources :collections, only: [:index]
resources :in_collections, only: [:index]
end
resources :async_refreshes, only: :show
resources :collections, only: [:show, :create, :update, :destroy] do
resources :items, only: [:create, :destroy], controller: 'collection_items' do
member do
@ -223,6 +226,9 @@ namespace :api, format: false do
resources :email_subscriptions, only: :create
end
resources :collections, only: [:index]
resources :in_collections, only: [:index]
member do
post :follow
post :unfollow
@ -329,6 +335,14 @@ namespace :api, format: false do
resources :tags, only: [:index, :show, :update]
end
resources :collections, only: [:show, :create, :update, :destroy] do
resources :items, only: [:create, :destroy], controller: 'collection_items' do
member do
post :revoke
end
end
end
end
namespace :v2 do

View File

@ -17,7 +17,7 @@
class: Scheduler::ScheduledStatusesScheduler
queue: scheduler
trends_refresh_scheduler:
every: '5m'
every: ['5m', first_in: '4m']
class: Scheduler::Trends::RefreshScheduler
queue: scheduler
trends_review_notifications_scheduler:

View File

@ -45,7 +45,7 @@ module Mastodon
def api_versions
{
mastodon: 9,
mastodon: 10,
glitch: 1,
}
end

View File

@ -182,7 +182,7 @@
"msw": "^2.12.1",
"msw-storybook-addon": "^2.0.6",
"oxfmt": "^0.47.0",
"playwright": "^1.57.0",
"playwright": "^1.60.0",
"react-test-renderer": "^18.2.0",
"storybook": "^10.3.0",
"stylelint": "^17.0.0",

View File

@ -172,7 +172,7 @@ RSpec.describe ActivityPub::Activity::Accept do
end
end
context 'with a FeatureRequest', feature: :collections do
context 'with a FeatureRequest' do
let(:collection) { Fabricate(:collection, account: recipient) }
let(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :pending) }
let(:object) { collection_item.activity_uri }

View File

@ -80,7 +80,7 @@ RSpec.describe ActivityPub::Activity::Add do
end
end
context 'when the target is the `featuredCollections` collection', feature: :collections do
context 'when the target is the `featuredCollections` collection' do
subject { described_class.new(activity_json, account) }
let(:account) { Fabricate(:remote_account, collections_url: 'https://example.com/actor/1/featured_collections') }
@ -122,7 +122,7 @@ RSpec.describe ActivityPub::Activity::Add do
end
end
context 'when the target is a collection', feature: :collections do
context 'when the target is a collection' do
subject { described_class.new(activity_json, collection.account) }
let(:collection) { Fabricate(:remote_collection) }

View File

@ -120,7 +120,7 @@ RSpec.describe ActivityPub::Activity::Delete do
end
end
context 'with a FeatureAuthorization', feature: :collections do
context 'with a FeatureAuthorization' do
let(:recipient) { Fabricate(:account) }
let(:approval_uri) { 'https://example.com/authorizations/1' }
let(:collection) { Fabricate(:collection, account: recipient) }

View File

@ -20,7 +20,7 @@ RSpec.describe ActivityPub::Activity::FeatureRequest do
}
end
describe '#perform', feature: :collections do
describe '#perform' do
subject { described_class.new(json, sender) }
context 'when recipient is discoverable' do

View File

@ -257,7 +257,7 @@ RSpec.describe ActivityPub::Activity::Update do
end
end
context 'with a `FeaturedCollection` object', feature: :collections do
context 'with a `FeaturedCollection` object' do
let(:collection) { Fabricate(:remote_collection, account: sender, name: 'old name', discoverable: false) }
let(:account) { Fabricate(:account) }
let!(:collection_item) { Fabricate(:collection_item, account:, collection:, uri: 'https://example.com/featured_stamps/1') }

View File

@ -71,16 +71,18 @@ RSpec.describe AdminMailer do
describe '.new_trends' do
let(:recipient) { Fabricate(:account, username: 'Snurf') }
let(:link) { Fabricate(:preview_card, trendable: true, language: 'en') }
let(:status) { Fabricate(:status) }
let(:tag) { Fabricate(:tag) }
let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag], [status]) }
let!(:link) { Fabricate(:preview_card, trendable: true, language: 'en') }
let!(:status) { Fabricate(:status) }
let!(:tag) { Fabricate(:tag, display_name: 'Test Tag') }
let!(:other_tag) { Fabricate(:tag, display_name: 'Other Test Tag') }
let(:mail) { described_class.with(recipient: recipient).new_trends([link], [tag, other_tag], [status]) }
let(:other_tag_trend) { Fabricate(:tag_trend, tag: other_tag) }
before do
PreviewCardTrend.create!(preview_card: link)
StatusTrend.create!(status: status, account: Fabricate(:account))
TagTrend.create!(tag: tag)
recipient.user.update(locale: :en)
Fabricate(:status_trend, status: status, account: Fabricate(:account))
Fabricate(:tag_trend, tag: tag)
Fabricate(:preview_card_trend, preview_card: link)
end
it 'renders the email' do
@ -96,6 +98,44 @@ RSpec.describe AdminMailer do
.and match(link.title)
.and match(tag.display_name)
end
context 'when between queueing and sending trends gets deleted' do
before do
recipient.user.update(locale: :en)
end
it 'sends the email when all but one trends were deleted without the respective tag or status or link' do
other_tag_trend
expect(mail.deliver_later!)
.to be_successfully_enqueued
TagTrend.delete_all
StatusTrend.delete_all
expect { mail.deliver }
.to send_email(
to: recipient.user_email,
from: 'notifications@localhost',
subject: I18n.t('admin_mailer.new_trends.subject', instance: Rails.configuration.x.local_domain)
)
expect(mail.body)
.to have_text(/The following items need a review before they can be displayed publicly/)
.and match(link.title)
.and not_include(ActivityPub::TagManager.instance.url_for(status))
.and not_include(tag.display_name)
end
it 'returns nil when no trends are present' do
expect(mail.deliver_later!)
.to be_successfully_enqueued
TagTrend.delete_all
StatusTrend.delete_all
PreviewCardTrend.delete_all
expect { mail.deliver }
.to_not send_email
end
end
end
describe '.new_software_updates' do

View File

@ -33,7 +33,7 @@ RSpec.describe Admin::ModerationAction do
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
context 'with attached collections' do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }
@ -47,7 +47,7 @@ RSpec.describe Admin::ModerationAction do
end
end
context 'with a remote collection', feature: :collections do
context 'with a remote collection' do
let(:status_ids) { [] }
let(:collection) { Fabricate(:remote_collection) }
let(:target_account) { collection.account }
@ -83,7 +83,7 @@ RSpec.describe Admin::ModerationAction do
expect(report.reload).to be_action_taken
end
context 'with attached collections', feature: :collections do
context 'with attached collections' do
let(:status_ids) { [] }
let(:collections) { Fabricate.times(2, :collection, account: target_account) }

View File

@ -3,7 +3,7 @@
require 'rails_helper'
RSpec.describe 'Collections' do
describe 'GET /ap/users/@:account_id/featured_collections', feature: :collections do
describe 'GET /ap/users/@:account_id/featured_collections' do
subject { get ap_account_featured_collections_path(account.id, format: :json) }
let(:collection) { Fabricate(:collection) }

View File

@ -47,7 +47,7 @@ RSpec.describe 'Admin Reports' do
it_behaves_like 'successful return'
end
context 'with a reported collection', feature: :collections do
context 'with a reported collection' do
before do
report.collections << Fabricate(:collection, account: report.target_account)
end
@ -55,7 +55,7 @@ RSpec.describe 'Admin Reports' do
it_behaves_like 'successful return'
end
context 'with both status and collection', feature: :collections do
context 'with both status and collection' do
before do
status = Fabricate(:status, account: report.target_account)
report.update(status_ids: [status.id])

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe 'Api::V1Alpha::CollectionItems', feature: :collections do
RSpec.describe 'Api::V1Alpha::CollectionItems' do
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
describe 'POST /api/v1_alpha/collections/:collection_id/items' do

View File

@ -2,12 +2,12 @@
require 'rails_helper'
RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
RSpec.describe 'Api::V1::Collections' do
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
describe 'GET /api/v1_alpha/accounts/:account_id/collections' do
describe 'GET /api/v1/accounts/:account_id/collections' do
subject do
get "/api/v1_alpha/accounts/#{account.id}/collections", headers: headers, params: params
get "/api/v1/accounts/#{account.id}/collections", headers: headers, params: params
end
let(:params) { {} }
@ -34,7 +34,7 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
expect(response)
.to include_pagination_headers(
next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 1)
next: api_v1_account_collections_url(account, limit: 1, offset: 1)
)
end
end
@ -50,8 +50,8 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
expect(response)
.to include_pagination_headers(
prev: api_v1_alpha_account_collections_url(account, limit: 1, offset: 0),
next: api_v1_alpha_account_collections_url(account, limit: 1, offset: 2)
prev: api_v1_account_collections_url(account, limit: 1, offset: 0),
next: api_v1_account_collections_url(account, limit: 1, offset: 2)
)
end
end
@ -96,9 +96,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
end
end
describe 'GET /api/v1_alpha/collections/:id' do
describe 'GET /api/v1/collections/:id' do
subject do
get "/api/v1_alpha/collections/#{collection.id}", headers: headers
get "/api/v1/collections/#{collection.id}", headers: headers
end
let(:collection) { Fabricate(:collection) }
@ -140,9 +140,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
end
end
describe 'POST /api/v1_alpha/collections' do
describe 'POST /api/v1/collections' do
subject do
post '/api/v1_alpha/collections', headers: headers, params: params
post '/api/v1/collections', headers: headers, params: params
end
let(:params) { {} }
@ -187,9 +187,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
end
end
describe 'PATCH /api/v1_alpha/collections/:id' do
describe 'PATCH /api/v1/collections/:id' do
subject do
patch "/api/v1_alpha/collections/#{collection.id}", headers: headers, params: params
patch "/api/v1/collections/#{collection.id}", headers: headers, params: params
end
let(:collection) { Fabricate(:collection) }
@ -256,9 +256,9 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do
end
end
describe 'DELETE /api/v1_alpha/collections/:id' do
describe 'DELETE /api/v1/collections/:id' do
subject do
delete "/api/v1_alpha/collections/#{collection.id}", headers: headers
delete "/api/v1/collections/#{collection.id}", headers: headers
end
let(:collection) { Fabricate(:collection) }

View File

@ -2,12 +2,12 @@
require 'rails_helper'
RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do
RSpec.describe 'Api::V1::InCollections' do
include_context 'with API authentication', oauth_scopes: 'read:collections write:collections'
describe 'GET /api/v1_alpha/in_collections' do
describe 'GET /api/v1/in_collections' do
subject do
get "/api/v1_alpha/accounts/#{account.id}/in_collections", headers: headers, params: params
get "/api/v1/accounts/#{account.id}/in_collections", headers: headers, params: params
end
let(:params) { {} }
@ -33,7 +33,7 @@ RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do
expect(response)
.to include_pagination_headers(
next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 1)
next: api_v1_account_in_collections_url(account, limit: 1, offset: 1)
)
end
end
@ -49,8 +49,8 @@ RSpec.describe 'Api::V1Alpha::InCollections', feature: :collections do
expect(response)
.to include_pagination_headers(
prev: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 0),
next: api_v1_alpha_account_in_collections_url(account, limit: 1, offset: 2)
prev: api_v1_account_in_collections_url(account, limit: 1, offset: 0),
next: api_v1_account_in_collections_url(account, limit: 1, offset: 2)
)
end
end

View File

@ -113,7 +113,7 @@ RSpec.describe 'Reports' do
end
end
context 'with attached collection', feature: :collections do
context 'with attached collection' do
let(:collection) { Fabricate(:collection, account: target_account) }
let(:collection_ids) { [collection.id] }

Some files were not shown because too many files have changed in this diff Show More