diff --git a/Gemfile.lock b/Gemfile.lock index 657c74e1ed..e655342dc4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -99,8 +99,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1225.0) - aws-sdk-core (3.243.0) + aws-partitions (1.1227.0) + aws-sdk-core (3.244.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -108,11 +108,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.122.0) - aws-sdk-core (~> 3, >= 3.241.4) + aws-sdk-kms (1.123.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.216.0) - aws-sdk-core (~> 3, >= 3.243.0) + aws-sdk-s3 (1.217.0) + aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -304,8 +304,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.27.0) - redis-client (= 0.27.0) + hiredis-client (0.28.0) + redis-client (= 0.28.0) hkdf (0.3.0) htmlentities (4.4.2) http (5.3.1) @@ -707,7 +707,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.27.0) + redis-client (0.28.0) connection_pool regexp_parser (2.11.3) reline (0.6.3) @@ -862,7 +862,7 @@ GEM unicode-display_width (>= 1.1.1, < 4) terrapin (1.1.1) climate_control - test-prof (1.5.2) + test-prof (1.6.0) thor (1.5.0) tilt (2.7.0) timeout (0.6.1) diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index c6c2a864a8..cb9109b769 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -158,7 +158,7 @@ export const expandPublicTimeline = ({ maxId, onlyMedia, onlyRemote } = export const expandCommunityTimeline = ({ maxId, onlyMedia } = {}) => expandTimeline(`community${onlyMedia ? ':media' : ''}`, '/api/v1/timelines/public', { local: true, max_id: maxId, only_media: !!onlyMedia }); export const expandAccountTimeline = (accountId, { maxId, withReplies, tagged } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, exclude_reblogs: withReplies, tagged, max_id: maxId }); export const expandAccountFeaturedTimeline = (accountId, { tagged } = {}) => expandTimeline(`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true, tagged }); -export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40 }); +export const expandAccountMediaTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}:media${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true, limit: 40, exclude_replies: !withReplies }); export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }); export const expandLinkTimeline = (url, { maxId } = {}) => expandTimeline(`link:${url}`, `/api/v1/timelines/link`, { url, max_id: maxId }); export const expandHashtagTimeline = (hashtag, { maxId, tags, local } = {}) => { diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index 39617d82fe..688c50d218 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -155,8 +155,12 @@ export async function apiRequest< export async function apiRequestGet( url: ApiUrl, params?: RequestParamsOrData, + args: { + signal?: AbortSignal; + timeout?: number; + } = {}, ) { - return apiRequest('GET', url, { params }); + return apiRequest('GET', url, { params, ...args }); } export async function apiRequestPost( diff --git a/app/javascript/mastodon/api/search.ts b/app/javascript/mastodon/api/search.ts index 79b0385fe8..497327004a 100644 --- a/app/javascript/mastodon/api/search.ts +++ b/app/javascript/mastodon/api/search.ts @@ -4,13 +4,22 @@ import type { ApiSearchResultsJSON, } from 'mastodon/api_types/search'; -export const apiGetSearch = (params: { - q: string; - resolve?: boolean; - type?: ApiSearchType; - limit?: number; - offset?: number; -}) => - apiRequestGet('v2/search', { - ...params, - }); +export const apiGetSearch = ( + params: { + q: string; + resolve?: boolean; + type?: ApiSearchType; + limit?: number; + offset?: number; + }, + options: { + signal?: AbortSignal; + } = {}, +) => + apiRequestGet( + 'v2/search', + { + ...params, + }, + options, + ); diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index 9fe076ce96..351f3245cc 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -53,6 +53,9 @@ export interface BaseApiAccountJSON { id: string; last_status_at: string; locked: boolean; + show_media: boolean; + show_media_replies: boolean; + show_featured: boolean; noindex?: boolean; note: string; roles?: ApiAccountJSON[]; diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx index a682dd9552..14f2d62d9a 100644 --- a/app/javascript/mastodon/components/follow_button.tsx +++ b/app/javascript/mastodon/components/follow_button.tsx @@ -6,7 +6,7 @@ import classNames from 'classnames'; import { Link } from 'react-router-dom'; import { useIdentity } from '@/mastodon/identity_context'; -import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { fetchRelationships, followAccount, @@ -171,7 +171,7 @@ export const FollowButton: React.FC<{ 'button--compact': compact, }); - if (isClientFeatureEnabled('profile_editing')) { + if (isServerFeatureEnabled('profile_redesign')) { return ( {label} diff --git a/app/javascript/mastodon/components/form_fields/combobox_field.tsx b/app/javascript/mastodon/components/form_fields/combobox_field.tsx index 8ce7161657..057258847e 100644 --- a/app/javascript/mastodon/components/form_fields/combobox_field.tsx +++ b/app/javascript/mastodon/components/form_fields/combobox_field.tsx @@ -28,7 +28,10 @@ export interface ComboboxItemState { isDisabled: boolean; } -interface ComboboxProps extends TextInputProps { +interface ComboboxProps extends Omit< + TextInputProps, + 'icon' +> { /** * The value of the combobox's text input */ @@ -71,6 +74,18 @@ interface ComboboxProps extends TextInputProps { * The main selection handler, called when an option is selected or deselected. */ onSelectItem: (item: T) => void; + /** + * Icon to be displayed in the text input + */ + icon?: TextInputProps['icon'] | null; + /** + * Set to false to keep the menu open when an item is selected + */ + closeOnSelect?: boolean; + /** + * Prevent the menu from opening, e.g. to prevent the empty state from showing + */ + suppressMenu?: boolean; } interface Props @@ -124,6 +139,8 @@ const ComboboxWithRef = ( onSelectItem, onChange, onKeyDown, + closeOnSelect = true, + suppressMenu = false, icon = SearchIcon, className, ...otherProps @@ -148,7 +165,7 @@ const ComboboxWithRef = ( const showStatusMessageInMenu = !!statusMessage && value.length > 0 && items.length === 0; const hasMenuContent = - !disabled && (items.length > 0 || showStatusMessageInMenu); + !disabled && !suppressMenu && (items.length > 0 || showStatusMessageInMenu); const isMenuOpen = shouldMenuOpen && hasMenuContent; const openMenu = useCallback(() => { @@ -204,11 +221,15 @@ const ComboboxWithRef = ( const isDisabled = getIsItemDisabled?.(item) ?? false; if (!isDisabled) { onSelectItem(item); + + if (closeOnSelect) { + closeMenu(); + } } } inputRef.current?.focus(); }, - [getIsItemDisabled, items, onSelectItem], + [closeMenu, closeOnSelect, getIsItemDisabled, items, onSelectItem], ); const handleSelectItem = useCallback( @@ -343,7 +364,7 @@ const ComboboxWithRef = ( value={value} onChange={handleInputChange} onKeyDown={handleInputKeyDown} - icon={icon} + icon={icon ?? undefined} className={classNames(classes.input, className)} ref={mergeRefs} /> diff --git a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx index f0bba5a745..9c67f4e4dd 100644 --- a/app/javascript/mastodon/features/account_edit/components/tag_search.tsx +++ b/app/javascript/mastodon/features/account_edit/components/tag_search.tsx @@ -1,95 +1,80 @@ import type { ChangeEventHandler, FC } from 'react'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useId, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import type { ApiHashtagJSON } from '@/mastodon/api_types/tags'; import { Combobox } from '@/mastodon/components/form_fields'; -import { - addFeaturedTag, - clearSearch, - updateSearchQuery, -} from '@/mastodon/reducers/slices/profile_edit'; -import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import { useSearchTags } from '@/mastodon/hooks/useSearchTags'; +import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags'; +import { addFeaturedTag } from '@/mastodon/reducers/slices/profile_edit'; +import { useAppDispatch } from '@/mastodon/store'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import classes from '../styles.module.scss'; -type SearchResult = Omit & { - label?: string; -}; - const messages = defineMessages({ placeholder: { id: 'account_edit_tags.search_placeholder', defaultMessage: 'Enter a hashtag…', }, - addTag: { - id: 'account_edit_tags.add_tag', - defaultMessage: 'Add #{tagName}', - }, }); export const AccountEditTagSearch: FC = () => { const intl = useIntl(); + const [query, setQuery] = useState(''); const { - query, + tags: suggestedTags, + searchTags, + resetSearch, isLoading, - results: rawResults, - } = useAppSelector((state) => state.profileEdit.search); - const results = useMemo(() => { - if (!rawResults) { - return []; - } + } = useSearchTags({ + query, + // Remove existing featured tags from suggestions + filterResults: (tag) => !tag.featuring, + }); - const results: SearchResult[] = [...rawResults]; // Make array mutable - const trimmedQuery = query.trim(); - if ( - trimmedQuery.length > 0 && - results.every( - (result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(), - ) - ) { - results.push({ - id: 'new', - name: trimmedQuery, - label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }), - }); - } - return results; - }, [intl, query, rawResults]); - - const dispatch = useAppDispatch(); const handleSearchChange: ChangeEventHandler = useCallback( (e) => { - void dispatch(updateSearchQuery(e.target.value)); + setQuery(e.target.value); + searchTags(e.target.value); }, - [dispatch], + [searchTags], ); + const dispatch = useAppDispatch(); const handleSelect = useCallback( - (item: SearchResult) => { - void dispatch(clearSearch()); + (item: TagSearchResult) => { + resetSearch(); + setQuery(''); void dispatch(addFeaturedTag({ name: item.name })); }, - [dispatch], + [dispatch, resetSearch], ); + const inputId = useId(); + const inputLabel = intl.formatMessage(messages.placeholder); + return ( - + <> + + + ); }; -const renderItem = (item: SearchResult) => item.label ?? `#${item.name}`; +const renderItem = (item: TagSearchResult) => item.label ?? `#${item.name}`; diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx index 57edf04b64..59e632aa10 100644 --- a/app/javascript/mastodon/features/account_featured/index.tsx +++ b/app/javascript/mastodon/features/account_featured/index.tsx @@ -2,10 +2,12 @@ import { useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { useParams } from 'react-router'; +import { useHistory } from 'react-router'; import { List as ImmutableList } from 'immutable'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { fetchEndorsedAccounts } from 'mastodon/actions/accounts'; import { fetchFeaturedTags } from 'mastodon/actions/featured_tags'; import { Account } from 'mastodon/components/account'; @@ -35,21 +37,27 @@ import { EmptyMessage } from './components/empty_message'; import { FeaturedTag } from './components/featured_tag'; import type { TagMap } from './components/featured_tag'; -interface Params { - acct?: string; - id?: string; -} - const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ multiColumn, }) => { const accountId = useAccountId(); + const account = useAccount(accountId); const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); const forceEmptyState = suspended || blockedBy || hidden; - const { acct = '' } = useParams(); const dispatch = useAppDispatch(); + const history = useHistory(); + useEffect(() => { + if ( + account && + !account.show_featured && + isServerFeatureEnabled('profile_redesign') + ) { + history.push(`/@${account.acct}`); + } + }, [account, history]); + useEffect(() => { if (accountId) { void dispatch(fetchFeaturedTags({ accountId })); @@ -166,7 +174,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({ aria-posinset={index + 1} aria-setsize={featuredTags.size} > - + ))} diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx index 594f71cb23..52f30ac505 100644 --- a/app/javascript/mastodon/features/account_gallery/index.tsx +++ b/app/javascript/mastodon/features/account_gallery/index.tsx @@ -2,10 +2,9 @@ import { useEffect, useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; -import { createSelector } from '@reduxjs/toolkit'; -import type { Map as ImmutableMap } from 'immutable'; -import { List as ImmutableList } from 'immutable'; +import { List as ImmutableList, isList } from 'immutable'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { openModal } from 'mastodon/actions/modal'; import { expandAccountMediaTimeline } from 'mastodon/actions/timelines'; import { ColumnBackButton } from 'mastodon/components/column_back_button'; @@ -18,38 +17,69 @@ import Column from 'mastodon/features/ui/components/column'; import { useAccountId } from 'mastodon/hooks/useAccountId'; import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import type { RootState } from 'mastodon/store'; -import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { + useAppSelector, + useAppDispatch, + createAppSelector, +} from 'mastodon/store'; import { MediaItem } from './components/media_item'; -const getAccountGallery = createSelector( +const emptyList = ImmutableList(); + +const redesignEnabled = isServerFeatureEnabled('profile_redesign'); + +const selectGalleryTimeline = createAppSelector( [ - (state: RootState, accountId: string) => - (state.timelines as ImmutableMap).getIn( - [`account:${accountId}:media`, 'items'], - ImmutableList(), - ) as ImmutableList, - (state: RootState) => state.statuses, + (_state, accountId?: string | null) => accountId, + (state) => state.timelines, + (state) => state.accounts, + (state) => state.statuses, ], - (statusIds, statuses) => { - let items = ImmutableList(); + (accountId, timelines, accounts, statuses) => { + if (!accountId) { + return null; + } + const account = accounts.get(accountId); + if (!account) { + return null; + } - statusIds.forEach((statusId) => { - const status = statuses.get(statusId) as - | ImmutableMap - | undefined; + let items = emptyList; + const { show_media, show_media_replies } = account; + // If the account disabled showing media, don't display anything. + if (!show_media && redesignEnabled) { + return { + items, + hasMore: false, + isLoading: false, + showingReplies: false, + }; + } - if (status) { + const showingReplies = show_media_replies && redesignEnabled; + const timeline = timelines.get( + `account:${accountId}:media${showingReplies ? ':with_replies' : ''}`, + ); + const statusIds = timeline?.get('items'); + + if (isList(statusIds)) { + for (const statusId of statusIds) { + const status = statuses.get(statusId); items = items.concat( ( - status.get('media_attachments') as ImmutableList + status?.get('media_attachments') as ImmutableList ).map((media) => media.set('status', status)), ); } - }); + } - return items; + return { + items, + hasMore: !!timeline?.get('hasMore'), + isLoading: !!timeline?.get('isLoading'), + showingReplies, + }; }, ); @@ -58,27 +88,12 @@ export const AccountGallery: React.FC<{ }> = ({ multiColumn }) => { const dispatch = useAppDispatch(); const accountId = useAccountId(); - const attachments = useAppSelector((state) => - accountId - ? getAccountGallery(state, accountId) - : ImmutableList(), - ); - const isLoading = useAppSelector((state) => - (state.timelines as ImmutableMap).getIn([ - `account:${accountId}:media`, - 'isLoading', - ]), - ); - const hasMore = useAppSelector((state) => - (state.timelines as ImmutableMap).getIn([ - `account:${accountId}:media`, - 'hasMore', - ]), - ); - const account = useAppSelector((state) => - accountId ? state.accounts.get(accountId) : undefined, - ); - const isAccount = !!account; + const { + isLoading = true, + hasMore = false, + items: attachments = emptyList, + showingReplies: withReplies = false, + } = useAppSelector((state) => selectGalleryTimeline(state, accountId)) ?? {}; const { suspended, blockedBy, hidden } = useAccountVisibility(accountId); @@ -87,16 +102,18 @@ export const AccountGallery: React.FC<{ | undefined; useEffect(() => { - if (accountId && isAccount) { - void dispatch(expandAccountMediaTimeline(accountId)); + if (accountId) { + void dispatch(expandAccountMediaTimeline(accountId, { withReplies })); } - }, [dispatch, accountId, isAccount]); + }, [dispatch, accountId, withReplies]); const handleLoadMore = useCallback(() => { if (maxId) { - void dispatch(expandAccountMediaTimeline(accountId, { maxId })); + void dispatch( + expandAccountMediaTimeline(accountId, { maxId, withReplies }), + ); } - }, [dispatch, accountId, maxId]); + }, [maxId, dispatch, accountId, withReplies]); const handleOpenMedia = useCallback( (attachment: MediaAttachment) => { diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx index eeb48c1c53..5febb8eaf8 100644 --- a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -5,25 +5,16 @@ import { FormattedMessage } from 'react-intl'; import type { NavLinkProps } from 'react-router-dom'; import { NavLink } from 'react-router-dom'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useAccountId } from '@/mastodon/hooks/useAccountId'; + import { isRedesignEnabled } from '../common'; import classes from './redesign.module.scss'; export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { if (isRedesignEnabled()) { - return ( -
- - - - - - - - - -
- ); + return ; } return (
@@ -49,3 +40,32 @@ export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { const isActive: Required['isActive'] = (match, location) => match?.url === location.pathname || (!!match?.url && location.pathname.startsWith(`${match.url}/tagged/`)); + +const RedesignTabs: FC = () => { + const accountId = useAccountId(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + const { acct, show_featured, show_media } = account; + + return ( +
+ + + + {show_media && ( + + + + )} + {show_featured && ( + + + + )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/collections/editor/accounts.tsx b/app/javascript/mastodon/features/collections/editor/accounts.tsx index 423b72e628..a6942193d0 100644 --- a/app/javascript/mastodon/features/collections/editor/accounts.tsx +++ b/app/javascript/mastodon/features/collections/editor/accounts.tsx @@ -25,8 +25,8 @@ import { ItemList, Scrollable, } from 'mastodon/components/scrollable_list/components'; -import { useSearchAccounts } from 'mastodon/features/lists/use_search_accounts'; import { useAccount } from 'mastodon/hooks/useAccount'; +import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts'; import { me } from 'mastodon/initial_state'; import { addCollectionItem, @@ -374,6 +374,7 @@ export const CollectionAccounts: React.FC<{ onSelectItem={ isEditMode ? instantToggleAccountItem : toggleAccountItem } + closeOnSelect={false} /> {hasMaxAccounts && ( { - const intl = useIntl(); const dispatch = useAppDispatch(); const history = useHistory(); const { id, name, description, topic, discoverable, sensitive, accountIds } = @@ -64,18 +69,6 @@ export const CollectionDetails: React.FC = () => { [dispatch], ); - const handleTopicChange = useCallback( - (event: React.ChangeEvent) => { - dispatch( - updateCollectionEditorField({ - field: 'topic', - value: inputToHashtag(event.target.value), - }), - ); - }, - [dispatch], - ); - const handleDiscoverableChange = useCallback( (event: React.ChangeEvent) => { dispatch( @@ -156,11 +149,6 @@ export const CollectionDetails: React.FC = () => { ], ); - const topicHasSpecialCharacters = useMemo( - () => hasSpecialCharacters(topic), - [topic], - ); - return (
@@ -213,39 +201,9 @@ export const CollectionDetails: React.FC = () => { maxLength={100} /> - - } - hint={ - - } - value={topic} - onChange={handleTopicChange} - autoCapitalize='off' - autoCorrect='off' - spellCheck='false' - maxLength={40} - status={ - topicHasSpecialCharacters - ? { - variant: 'warning', - message: intl.formatMessage({ - id: 'collections.topic_special_chars_hint', - defaultMessage: - 'Special characters will be removed when saving', - }), - } - : undefined - } - /> + + +
{ ); }; + +const TopicField: React.FC = () => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const { id, topic } = useAppSelector((state) => state.collections.editor); + + const collection = useAppSelector((state) => + id ? state.collections.collections[id] : undefined, + ); + const [isInitialValue, setIsInitialValue] = useState( + () => trimHashFromStart(topic) === (collection?.tag?.name ?? ''), + ); + + const { tags, isLoading, searchTags } = useSearchTags({ + query: topic, + }); + + const handleTopicChange = useCallback( + (event: React.ChangeEvent) => { + setIsInitialValue(false); + dispatch( + updateCollectionEditorField({ + field: 'topic', + value: inputToHashtag(event.target.value), + }), + ); + searchTags(event.target.value); + }, + [dispatch, searchTags], + ); + + const handleSelectTopicSuggestion = useCallback( + (item: TagSearchResult) => { + dispatch( + updateCollectionEditorField({ + field: 'topic', + value: inputToHashtag(item.name), + }), + ); + }, + [dispatch], + ); + + const topicHasSpecialCharacters = useMemo( + () => hasSpecialCharacters(topic), + [topic], + ); + + return ( + + } + hint={ + + } + value={topic} + items={tags} + isLoading={isLoading} + renderItem={renderTagItem} + onSelectItem={handleSelectTopicSuggestion} + onChange={handleTopicChange} + autoCapitalize='off' + autoCorrect='off' + spellCheck='false' + maxLength={40} + status={ + topicHasSpecialCharacters + ? { + variant: 'warning', + message: intl.formatMessage({ + id: 'collections.topic_special_chars_hint', + defaultMessage: + 'Special characters will be removed when saving', + }), + } + : undefined + } + suppressMenu={isInitialValue} + /> + ); +}; + +const renderTagItem = (item: TagSearchResult) => item.label ?? `#${item.name}`; + +const LanguageField: React.FC = () => { + const dispatch = useAppDispatch(); + const initialLanguage = useAppSelector( + (state) => state.compose.get('default_language') as string, + ); + const { language } = useAppSelector((state) => state.collections.editor); + + const selectedLanguage = language ?? initialLanguage; + + const handleLanguageChange = useCallback( + (event: React.ChangeEvent) => { + dispatch( + updateCollectionEditorField({ + field: 'language', + value: event.target.value, + }), + ); + }, + [dispatch], + ); + + return ( + + } + value={selectedLanguage} + onChange={handleLanguageChange} + > + + {languages?.map(([code, name, localName]) => ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/link_timeline/index.tsx b/app/javascript/mastodon/features/link_timeline/index.tsx index e6b8480a24..fd9e157773 100644 --- a/app/javascript/mastodon/features/link_timeline/index.tsx +++ b/app/javascript/mastodon/features/link_timeline/index.tsx @@ -21,8 +21,7 @@ export const LinkTimeline: React.FC<{ const columnRef = useRef(null); const firstStatusId = useAppSelector((state) => decodedUrl - ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) + ? (state.timelines.getIn([`link:${decodedUrl}`, 'items', 0]) as string) : undefined, ); const story = useAppSelector((state) => diff --git a/app/javascript/mastodon/features/lists/members.tsx b/app/javascript/mastodon/features/lists/members.tsx index c0c6ea54f0..c8970b6d7a 100644 --- a/app/javascript/mastodon/features/lists/members.tsx +++ b/app/javascript/mastodon/features/lists/members.tsx @@ -28,11 +28,10 @@ import { DisplayName } from 'mastodon/components/display_name'; import ScrollableList from 'mastodon/components/scrollable_list'; import { ShortNumber } from 'mastodon/components/short_number'; import { VerifiedBadge } from 'mastodon/components/verified_badge'; +import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts'; import { me } from 'mastodon/initial_state'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { useSearchAccounts } from './use_search_accounts'; - export const messages = defineMessages({ manageMembers: { id: 'column.list_members', diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index eae6d35a5f..7540d64b4e 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -23,7 +23,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report'; -import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; +import { isServerFeatureEnabled } from '@/mastodon/utils/environment'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; @@ -182,7 +182,7 @@ class SwitchingColumnsArea extends PureComponent { } const profileRedesignRoutes = []; - if (isClientFeatureEnabled('profile_editing')) { + if (isServerFeatureEnabled('profile_redesign')) { profileRedesignRoutes.push( , diff --git a/app/javascript/mastodon/features/lists/use_search_accounts.ts b/app/javascript/mastodon/hooks/useSearchAccounts.ts similarity index 100% rename from app/javascript/mastodon/features/lists/use_search_accounts.ts rename to app/javascript/mastodon/hooks/useSearchAccounts.ts diff --git a/app/javascript/mastodon/hooks/useSearchTags.ts b/app/javascript/mastodon/hooks/useSearchTags.ts new file mode 100644 index 0000000000..2f029b07e8 --- /dev/null +++ b/app/javascript/mastodon/hooks/useSearchTags.ts @@ -0,0 +1,121 @@ +import { useCallback, useMemo, useRef, useState } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { useDebouncedCallback } from 'use-debounce'; + +import { apiGetSearch } from 'mastodon/api/search'; +import type { ApiHashtagJSON } from 'mastodon/api_types/tags'; +import { trimHashFromStart } from 'mastodon/utils/hashtags'; + +export type TagSearchResult = Omit & { + label?: string; +}; + +const messages = defineMessages({ + addTag: { + id: 'account_edit_tags.add_tag', + defaultMessage: 'Add #{tagName}', + }, +}); + +const fetchSearchHashtags = ({ + q, + limit, + signal, +}: { + q: string; + limit: number; + signal: AbortSignal; +}) => apiGetSearch({ q, type: 'hashtags', limit }, { signal }); + +export function useSearchTags({ + query, + limit = 11, + filterResults, +}: { + query?: string; + limit?: number; + filterResults?: (account: ApiHashtagJSON) => boolean; +} = {}) { + const intl = useIntl(); + const [fetchedTags, setFetchedTags] = useState([]); + const [loadingState, setLoadingState] = useState< + 'idle' | 'loading' | 'error' + >('idle'); + + const searchRequestRef = useRef(null); + + const searchTags = useDebouncedCallback( + (value: string) => { + if (searchRequestRef.current) { + searchRequestRef.current.abort(); + } + + const trimmedQuery = trimHashFromStart(value.trim()); + + if (trimmedQuery.length === 0) { + setFetchedTags([]); + return; + } + + setLoadingState('loading'); + + searchRequestRef.current = new AbortController(); + + void fetchSearchHashtags({ + q: trimmedQuery, + limit, + signal: searchRequestRef.current.signal, + }) + .then(({ hashtags }) => { + const tags = filterResults + ? hashtags.filter(filterResults) + : hashtags; + setFetchedTags(tags); + setLoadingState('idle'); + }) + .catch(() => { + setLoadingState('error'); + }); + }, + 500, + { leading: true, trailing: true }, + ); + + const resetSearch = useCallback(() => { + setFetchedTags([]); + setLoadingState('idle'); + }, []); + + // Add dedicated item for adding the current query + const tags = useMemo(() => { + const trimmedQuery = query ? trimHashFromStart(query.trim()) : ''; + if (!trimmedQuery || !fetchedTags.length) { + return fetchedTags; + } + + const results: TagSearchResult[] = [...fetchedTags]; // Make array mutable + if ( + trimmedQuery.length > 0 && + results.every( + (result) => result.name.toLowerCase() !== trimmedQuery.toLowerCase(), + ) + ) { + results.push({ + id: 'new', + name: trimmedQuery, + label: intl.formatMessage(messages.addTag, { tagName: trimmedQuery }), + }); + } + return results; + }, [fetchedTags, query, intl]); + + return { + tags, + searchTags, + resetSearch, + isLoading: loadingState === 'loading', + isError: loadingState === 'error', + }; +} diff --git a/app/javascript/mastodon/initial_state.ts b/app/javascript/mastodon/initial_state.ts index 9af0c26f93..0493fcf622 100644 --- a/app/javascript/mastodon/initial_state.ts +++ b/app/javascript/mastodon/initial_state.ts @@ -153,7 +153,7 @@ export const languages = initialState?.languages.map((lang) => { lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1], lang[2], - ]; + ] as InitialStateLanguage; }); export function getAccessToken(): string | undefined { diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 3b2f982c12..ac7bd45464 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -39,7 +39,7 @@ "account.edit_profile_short": "Рэдагаваць", "account.enable_notifications": "Апавяшчаць мяне пра допісы @{name}", "account.endorse": "Паказваць у профілі", - "account.familiar_followers_many": "Мае сярод падпісчыкаў {name1}, {name2}, і {othersCount, plural, one {яшчэ # чалавека, знаёмага Вам} few {яшчэ # чалавекі, знаёмыя Вам} many {яшчэ # чалавек, знаёмых Вам} other {яшчэ # чалавекі, знаёмыя Вам}}", + "account.familiar_followers_many": "Мае сярод падпісчыкаў {name1}, {name2}, і {othersCount, plural, one {яшчэ #-го чалавека, знаёмага Вам} few {яшчэ #-х чалавек, знаёмых Вам} many {яшчэ # людзей, знаёмых Вам} other {яшчэ # чалавек, знаёмых Вам}}", "account.familiar_followers_one": "Мае сярод падпісчыкаў {name1}", "account.familiar_followers_two": "Мае сярод падпісчыкаў {name1} і {name2}", "account.featured": "Рэкамендаванае", @@ -73,7 +73,6 @@ "account.go_to_profile": "Перайсці да профілю", "account.hide_reblogs": "Схаваць пашырэнні ад @{name}", "account.in_memoriam": "У памяць.", - "account.joined_long": "Далучыў(-ла)ся {date}", "account.joined_short": "Далучыўся", "account.languages": "Змяніць выбраныя мовы", "account.link_verified_on": "Права ўласнасці на гэтую спасылку праверана {date}", @@ -151,13 +150,53 @@ "account_edit.button.edit": "Змяніць {item}", "account_edit.column_button": "Гатова", "account_edit.column_title": "Рэдагаваць профіль", + "account_edit.custom_fields.name": "поле", "account_edit.custom_fields.placeholder": "Дадайце свае займеннікі, знешнія спасылкі ці нешта іншае, чым Вы хацелі б падзяліцца.", - "account_edit.custom_fields.title": "Карыстальніцкія палі", + "account_edit.custom_fields.reorder_button": "Змяніць парадак палёў", + "account_edit.custom_fields.tip_content": "Вы можаце лёгка дадаць даверу да свайго ўліковага запісу Mastodon пацвярджэннем спасылак на любы з Вашых сайтаў.", + "account_edit.custom_fields.tip_title": "Падказка: Дадаванне пацверджаных спасылак", + "account_edit.custom_fields.title": "Адвольныя палі", + "account_edit.custom_fields.verified_hint": "Як мне дадаць пацверджаную спасылку?", "account_edit.display_name.placeholder": "Вашае бачнае імя — гэта імя, якое іншыя людзі бачаць у Вашым профілі і ў стужках.", "account_edit.display_name.title": "Бачнае імя", "account_edit.featured_hashtags.item": "хэштэгі", "account_edit.featured_hashtags.placeholder": "Дапамажыце іншым зразумець, якія тэмы Вас цікавяць, і атрымаць доступ да іх.", "account_edit.featured_hashtags.title": "Выбраныя хэштэгі", + "account_edit.field_delete_modal.confirm": "Вы ўпэўненыя, што хочаце выдаліць гэтае адвольнае поле? Гэтае дзеянне будзе незваротным.", + "account_edit.field_delete_modal.delete_button": "Выдаліць", + "account_edit.field_delete_modal.title": "Выдаліць адвольнае поле?", + "account_edit.field_edit_modal.add_title": "Дадаць адвольнае поле", + "account_edit.field_edit_modal.edit_title": "Рэдагаваць адвольнае поле", + "account_edit.field_edit_modal.limit_header": "Перавышаная рэкамендаваная колькасць сімвалаў", + "account_edit.field_edit_modal.limit_message": "Карыстальнікі мабільных прылад могуць не ўбачыць Вашае поле цалкам.", + "account_edit.field_edit_modal.link_emoji_warning": "Мы раім не ўжываць адвольныя эмодзі разам з url-спасылкамі. Адвольныя палі, якія ўтрымліваюць і тое, і другое, будуць адлюстраваныя выключна як тэкст, а не спасылкі, каб не блытаць карыстальнікаў.", + "account_edit.field_edit_modal.name_hint": "Напрыклад, \"Асабісты Сайт\"", + "account_edit.field_edit_modal.name_label": "Назва", + "account_edit.field_edit_modal.url_warning": "Каб дадаць спасылку, калі ласка, дадайце {protocol} у яе пачатку.", + "account_edit.field_edit_modal.value_hint": "Напрыклад, “https://example.me”", + "account_edit.field_edit_modal.value_label": "Значэнне", + "account_edit.field_reorder_modal.drag_cancel": "Перасоўванне адмененае. Поле {item} было павернутае на месца.", + "account_edit.field_reorder_modal.drag_end": "Поле {item} было павернутае на месца.", + "account_edit.field_reorder_modal.drag_instructions": "Каб змяніць парадак адвольных палёў, націсніце прабел або ўвод. Падчас перасоўвання карыстайцеся клавішамі са стрэлкамі, каб пасунуць поле ўверх ці ўніз. Націсніце прабел ці ўвод зноў, каб замацаваць поле на новым месцы, або націсніце Esc, каб скасаваць дзеянне.", + "account_edit.field_reorder_modal.drag_move": "Поле {item} было перасунутае.", + "account_edit.field_reorder_modal.drag_over": "Поле {item} было перасунутае над \"{over}\".", + "account_edit.field_reorder_modal.drag_start": "Абранае поле \"{item}\".", + "account_edit.field_reorder_modal.handle_label": "Перасунуць поле \"{item}\"", + "account_edit.field_reorder_modal.title": "Перасунуць палі", + "account_edit.image_alt_modal.add_title": "Дадаць альт. тэкст", + "account_edit.image_alt_modal.details_content": "ЯК РАБІЦЬ:
  • Апішыце, як Вы выглядаеце на відарысе
  • Апісвайце ад трэцяй асобы (напрыклад, “Алесь” замест \"я”)
  • Будзьце сціслымі — некалькі слоў звычайна дастаткова
ЯК НЕ РАБІЦЬ:
  • Пачынаць з \"Фотаздымак...\" — гэта залішняе для чытачоў
ПРЫКЛАД:
  • “Алесь у вышыванцы і акулярах”
", + "account_edit.image_alt_modal.details_title": "Падказка: Альтэрнатыўны тэкст для фота профілю", + "account_edit.image_alt_modal.edit_title": "Рэдагаваць альт. тэкст", + "account_edit.image_alt_modal.text_hint": "Альтэрнатыўны тэкст дапамагае чытачам Вашага кантэнту лепш яго разумець.", + "account_edit.image_alt_modal.text_label": "Альт. тэкст", + "account_edit.image_delete_modal.confirm": "Вы ўпэўненыя, што хочаце выдаліць гэты відарыс? Гэтае дзеянне будзе незваротным.", + "account_edit.image_delete_modal.delete_button": "Выдаліць", + "account_edit.image_delete_modal.title": "Выдаліць відарыс?", + "account_edit.image_edit.add_button": "Дадаць відарыс", + "account_edit.image_edit.alt_add_button": "Дадаць альт. тэкст", + "account_edit.image_edit.alt_edit_button": "Рэдагаваць альт. тэкст", + "account_edit.image_edit.remove_button": "Прыбраць відарыс", + "account_edit.image_edit.replace_button": "Замяніць відарыс", "account_edit.name_modal.add_title": "Дадаць бачнае імя", "account_edit.name_modal.edit_title": "Змяніць бачнае імя", "account_edit.profile_tab.button_label": "Змяніць", @@ -172,6 +211,23 @@ "account_edit.profile_tab.subtitle": "Змяняйце на свой густ укладкі свайго профілю і тое, што яны паказваюць.", "account_edit.profile_tab.title": "Налады ўкладкі профілю", "account_edit.save": "Захаваць", + "account_edit.upload_modal.back": "Назад", + "account_edit.upload_modal.done": "Гатова", + "account_edit.upload_modal.next": "Далей", + "account_edit.upload_modal.step_crop.zoom": "Маштаб", + "account_edit.upload_modal.step_upload.button": "Агляд файлаў", + "account_edit.upload_modal.step_upload.dragging": "Перацягн. сюды, каб запамп.", + "account_edit.upload_modal.step_upload.header": "Выбраць відарыс", + "account_edit.upload_modal.step_upload.hint": "Фармату WEBP, PNG, GIF або JPG, да {limit} МБ.{br}Відарыс будзе сціснуты да памеру {width}x{height} пікселяў.", + "account_edit.upload_modal.title_add": "Дадаць фота профілю", + "account_edit.upload_modal.title_replace": "Замяніць фота профілю", + "account_edit.verified_modal.details": "Дадайце даверу да Вашага профілю Mastodon, пацвярджэннем спасылак на ўласныя сайты. Вось як гэта працуе:", + "account_edit.verified_modal.invisible_link.details": "Дадайце спасылку ў свой загаловак. Важнай часткай з'яўляецца rel=\"me\", яна прадухіляе выдачу сябе за іншую асобу на сайтах з карыстальніцкім кантэнтам. Вы нават можаце выкарыстоўваць тэг link у загалоўку старонкі замест {tag}, але HTML код павінен быць даступным без выканання JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Як мне зрабіць спасылку нябачнай?", + "account_edit.verified_modal.step1.header": "Скапіруйце HTML код знізу і ўстаўце яго ў загаловак свайго сайту", + "account_edit.verified_modal.step2.details": "Калі Вы ўжо дадалі свой сайт у якасці адвольнага поля, Вам спатрэбіцца яго выдаліць і зноў дадаць, каб запусціць верыфікацыю.", + "account_edit.verified_modal.step2.header": "Дадаць свой сайт як адвольнае поле", + "account_edit.verified_modal.title": "Як дадаць пацверджаную спасылку", "account_edit_tags.add_tag": "Дадаць #{tagName}", "account_edit_tags.column_title": "Змяніць выбраныя хэштэгі", "account_edit_tags.help_text": "Выбраныя хэштэгі дапамагаюць карыстальнікам знаходзіць Ваш профіль і ўзаемадзейнічаць з ім. Яны дзейнічаюць як фільтры пры праглядзе актыўнасці на Вашай старонцы.", @@ -306,10 +362,15 @@ "collections.create_collection": "Стварыць калекцыю", "collections.delete_collection": "Выдаліць калекцыю", "collections.description_length_hint": "Максімум 100 сімвалаў", + "collections.detail.accept_inclusion": "Добра", "collections.detail.accounts_heading": "Уліковыя запісы", + "collections.detail.author_added_you": "{author} дадаў(-ла) Вас у гэтую калекцыю", "collections.detail.curated_by_author": "Курыруе {author}", "collections.detail.curated_by_you": "Курыруеце Вы", "collections.detail.loading": "Загружаецца калекцыя…", + "collections.detail.other_accounts_in_collection": "Іншыя ў гэтай калекцыі:", + "collections.detail.revoke_inclusion": "Прыбраць сябе", + "collections.detail.sensitive_note": "У гэтай калекцыі прысутнічаюць уліковыя запісы і кантэнт, змесціва якіх можа падацца адчувальным для некаторых карыстальнікаў.", "collections.detail.share": "Падзяліцца гэтай калекцыяй", "collections.edit_details": "Рэдагаваць падрабязнасці", "collections.error_loading_collections": "Адбылася памылка падчас загрузкі Вашых калекцый.", @@ -324,10 +385,14 @@ "collections.old_last_post_note": "Апошні допіс быў больш за тыдзень таму", "collections.remove_account": "Прыбраць гэты ўліковы запіс", "collections.report_collection": "Паскардзіцца на гэту калекцыю", + "collections.revoke_collection_inclusion": "Прыбраць сябе з гэтай калекцыі", + "collections.revoke_inclusion.confirmation": "Вас прыбралі з \"{collection}\"", + "collections.revoke_inclusion.error": "Адбылася памылка, калі ласка, спрабуйце яшчэ раз пазней.", "collections.search_accounts_label": "Шукайце ўліковыя запісы, каб дадаць іх сюды…", "collections.search_accounts_max_reached": "Вы дадалі максімальную колькасць уліковых запісаў", "collections.sensitive": "Адчувальная", "collections.topic_hint": "Дадайце хэштэг, які дапаможа іншым зразумець галоўную тэму гэтай калекцыі.", + "collections.topic_special_chars_hint": "Спецыяльныя сімвалы будуць прыбраныя пры захаванні", "collections.view_collection": "Глядзець калекцыю", "collections.view_other_collections_by_user": "Паглядзець іншыя калекцыі гэтага карыстальніка", "collections.visibility_public": "Публічная", @@ -447,6 +512,9 @@ "confirmations.remove_from_followers.confirm": "Выдаліць падпісчыка", "confirmations.remove_from_followers.message": "{name} больш не будзе падпісаны(-ая) на Вас. Упэўненыя, што хочаце працягнуць?", "confirmations.remove_from_followers.title": "Выдаліць падпісчыка?", + "confirmations.revoke_collection_inclusion.confirm": "Прыбраць сябе", + "confirmations.revoke_collection_inclusion.message": "Гэтае дзеянне канчатковае, і куратар не зможа пасля зноў дадаць Вас у гэтую калекцыю.", + "confirmations.revoke_collection_inclusion.title": "Прыбраць Вас з гэтай калекцыі?", "confirmations.revoke_quote.confirm": "Выдаліць допіс", "confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.", "confirmations.revoke_quote.title": "Выдаліць допіс?", @@ -769,6 +837,7 @@ "navigation_bar.automated_deletion": "Аўтаматычнае выдаленне допісаў", "navigation_bar.blocks": "Заблакіраваныя карыстальнікі", "navigation_bar.bookmarks": "Закладкі", + "navigation_bar.collections": "Калекцыі", "navigation_bar.direct": "Прыватныя згадванні", "navigation_bar.domain_blocks": "Заблакіраваныя дамены", "navigation_bar.favourites": "Упадабанае", @@ -916,12 +985,14 @@ "notifications_permission_banner.title": "Не прапусціце нічога", "onboarding.follows.back": "Назад", "onboarding.follows.empty": "На жаль, зараз немагчыма паказаць вынікі. Вы можаце паспрабаваць выкарыстоўваць пошук і праглядзець старонку агляду, каб знайсці людзей, на якіх можна падпісацца, або паўтарыць спробу пазней.", + "onboarding.follows.next": "Далей: Наладзьце свой профіль", "onboarding.follows.search": "Пошук", "onboarding.follows.title": "Падпішыцеся на некага, каб пачаць", "onboarding.profile.discoverable": "Зрабіць мой профіль бачным", "onboarding.profile.discoverable_hint": "Калі Вы звяртаецеся да адкрытасці на Mastodon, Вашы допісы могуць з'яўляцца ў выніках пошуку і трэндах, а Ваш профіль можа быць прапанаваны людзям з такімі ж інтарэсамі.", "onboarding.profile.display_name": "Бачнае імя", "onboarding.profile.display_name_hint": "Ваша поўнае імя або ваш псеўданім…", + "onboarding.profile.finish": "Гатова", "onboarding.profile.note": "Біяграфія", "onboarding.profile.note_hint": "Вы можаце @згадваць іншых людзей або выкарыстоўваць #хэштэгі…", "onboarding.profile.title": "Налады профілю", diff --git a/app/javascript/mastodon/locales/ca.json b/app/javascript/mastodon/locales/ca.json index 029c61cf06..08b644dd52 100644 --- a/app/javascript/mastodon/locales/ca.json +++ b/app/javascript/mastodon/locales/ca.json @@ -70,7 +70,6 @@ "account.go_to_profile": "Vés al perfil", "account.hide_reblogs": "Amaga els impulsos de @{name}", "account.in_memoriam": "En Memòria.", - "account.joined_long": "Membre des de {date}", "account.joined_short": "S'hi va unir", "account.languages": "Canvia les llengües subscrites", "account.link_verified_on": "La propietat d'aquest enllaç es va verificar el dia {date}", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 3ab9bcc5f3..02fe2ebc19 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -71,7 +71,6 @@ "account.go_to_profile": "Přejít na profil", "account.hide_reblogs": "Skrýt boosty od @{name}", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "Přidali se {date}", "account.joined_short": "Připojen/a", "account.languages": "Změnit odebírané jazyky", "account.link_verified_on": "Vlastnictví tohoto odkazu bylo zkontrolováno {date}", diff --git a/app/javascript/mastodon/locales/cy.json b/app/javascript/mastodon/locales/cy.json index 48bda093f9..ccb9375d7f 100644 --- a/app/javascript/mastodon/locales/cy.json +++ b/app/javascript/mastodon/locales/cy.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Mynd i'r proffil", "account.hide_reblogs": "Cuddio hybiau gan @{name}", "account.in_memoriam": "Er Cof.", - "account.joined_long": "Ymunodd ar {date}", "account.joined_short": "Ymunodd", "account.languages": "Newid ieithoedd wedi tanysgrifio iddyn nhw", "account.link_verified_on": "Gwiriwyd perchnogaeth y ddolen yma ar {date}", @@ -183,6 +182,15 @@ "account_edit.field_reorder_modal.drag_start": "Wedi codi maes \"{item}\".", "account_edit.field_reorder_modal.handle_label": "Llusgo'r maes \"{item}\"", "account_edit.field_reorder_modal.title": "Aildrefnu meysydd", + "account_edit.image_alt_modal.add_title": "Ychwanegu testun amgen", + "account_edit.image_alt_modal.details_content": "GWNEUD:
  • Disgrifio'ch hun fel yn eich llun
  • Defnyddio iaith trydydd person (e.e. “Siôn” yn lle “fi”)
  • Bod yn gryno – mae ychydig o eiriau’n aml yn ddigon
PEIDIO:
  • Dechrau gyda “Llun o” – mae’n ddiangen ar gyfer darllenwyr sgrin
ENGHRAIFFT:
  • “Dyma Siôn yn gwisgo crys gwyrdd a sbectol”
", + "account_edit.image_alt_modal.details_title": "Awgrymiadau: Testun amgen ar gyfer lluniau proffil", + "account_edit.image_alt_modal.edit_title": "Golygu testun amgen", + "account_edit.image_alt_modal.text_hint": "Mae testun amgen yn helpu defnyddwyr darllenwyr sgrin i ddeall eich cynnwys.", + "account_edit.image_alt_modal.text_label": "Testun amgen", + "account_edit.image_delete_modal.confirm": "Ydych chi'n siŵr eich bod chi eisiau dileu'r ddelwedd hon? Does dim modd dadwneud hynny.", + "account_edit.image_delete_modal.delete_button": "Dileu", + "account_edit.image_delete_modal.title": "Dileu delwedd?", "account_edit.image_edit.add_button": "Ychwanegu Delwedd", "account_edit.image_edit.alt_add_button": "Ychwanegu testun amgen", "account_edit.image_edit.alt_edit_button": "Golygu testun amgen", @@ -202,6 +210,16 @@ "account_edit.profile_tab.subtitle": "Cyfaddaswch y tabiau ar eich proffil a'r hyn maen nhw'n ei ddangos.", "account_edit.profile_tab.title": "Gosodiadau tab proffil", "account_edit.save": "Cadw", + "account_edit.upload_modal.back": "Nôl", + "account_edit.upload_modal.done": "Gorffen", + "account_edit.upload_modal.next": "Nesaf", + "account_edit.upload_modal.step_crop.zoom": "Chwyddo", + "account_edit.upload_modal.step_upload.button": "Pori ffeiliau", + "account_edit.upload_modal.step_upload.dragging": "Gollwng i lwytho i fyny", + "account_edit.upload_modal.step_upload.header": "Dewiswch ddelwedd", + "account_edit.upload_modal.step_upload.hint": "Fformat WEBP, PNG, GIF neu JPG, hyd at {limit}MB.{br}Bydd y ddelwedd yn cael ei haddasu i {width}x{height}px.", + "account_edit.upload_modal.title_add": "Ychwanegu llun proffil", + "account_edit.upload_modal.title_replace": "Amnewid llun proffil", "account_edit.verified_modal.details": "Ychwanegwch hygrededd at eich proffil Mastodon trwy wirio dolenni i wefannau personol. Dyma sut mae'n gweithio:", "account_edit.verified_modal.invisible_link.details": "Ychwanegwch y ddolen at eich pennyn. Y rhan bwysig yw rel=\"me\" sy'n atal dynwared ar wefannau gyda chynnwys sy'n cael ei gynhyrchu gan ddefnyddwyr. Gallwch hyd yn oed ddefnyddio tag dolen ym mhennyn y dudalen yn lle {tag}, ond rhaid bod yr HTML yn hygyrch ac heb weithredu JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Sut ydw i'n gwneud y ddolen yn anweledig?", @@ -373,6 +391,7 @@ "collections.search_accounts_max_reached": "Rydych chi wedi ychwanegu'r nifer mwyaf o gyfrifon", "collections.sensitive": "Sensitif", "collections.topic_hint": "Ychwanegwch hashnod sy'n helpu eraill i ddeall prif bwnc y casgliad hwn.", + "collections.topic_special_chars_hint": "Bydd nodau arbennig yn cael eu tynnu wrth gadw", "collections.view_collection": "Gweld y casgliad", "collections.view_other_collections_by_user": "Edrychwch ar gasgliadau eraill gan y defnyddiwr hwn", "collections.visibility_public": "Cyhoeddus", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 8299aadcfb..1d6cfb1a52 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul fremhævelser fra @{name}", "account.in_memoriam": "Til minde om.", - "account.joined_long": "Tilmeldt {date}", "account.joined_short": "Oprettet", "account.languages": "Skift abonnementssprog", "account.link_verified_on": "Ejerskab af dette link blev tjekket {date}", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index ed31db9adc..49bbe30b87 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Profil aufrufen", "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden", "account.in_memoriam": "Zum Andenken.", - "account.joined_long": "Registriert am {date}", "account.joined_short": "Registriert am", "account.languages": "Sprachen verwalten", "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 6a1649f253..41da6332ef 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Μετάβαση στο προφίλ", "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}", "account.in_memoriam": "Εις μνήμην.", - "account.joined_long": "Έγινε μέλος {date}", "account.joined_short": "Έγινε μέλος", "account.languages": "Αλλαγή εγγεγραμμένων γλωσσών", "account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου ελέγχθηκε στις {date}", @@ -214,6 +213,7 @@ "account_edit.save": "Αποθήκευση", "account_edit.upload_modal.back": "Πίσω", "account_edit.upload_modal.done": "Έγινε", + "account_edit.upload_modal.next": "Επόμενο", "account_edit.upload_modal.step_crop.zoom": "Μεγέθυνση", "account_edit.upload_modal.step_upload.button": "Περιήγηση αρχείων", "account_edit.upload_modal.step_upload.dragging": "Αποθέστε εδώ για ανέβασμα", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 8e56d07168..fb3bc2b7d8 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f3b35a7741..e3feb120a2 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -349,6 +349,8 @@ "collections.accounts.empty_description": "Add up to {count} accounts you follow", "collections.accounts.empty_title": "This collection is empty", "collections.collection_description": "Description", + "collections.collection_language": "Language", + "collections.collection_language_none": "None", "collections.collection_name": "Name", "collections.collection_topic": "Topic", "collections.confirm_account_removal": "Are you sure you want to remove this account from this collection?", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index b6fa0195e1..5a9de67ffe 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar adhesiones de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", - "account.joined_long": "En este servidor desde el {date}", "account.joined_short": "En este servidor desde el", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 8a92624f3f..fe5cde92cf 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "En memoria.", - "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "Se verificó la propiedad de este enlace el {date}", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index d0c59b2acf..e4f7519540 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", - "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -184,6 +183,15 @@ "account_edit.field_reorder_modal.drag_start": "Campo \"{item}\" seleccionado.", "account_edit.field_reorder_modal.handle_label": "Arrastra el campo \"{item}\"", "account_edit.field_reorder_modal.title": "Reorganizar campos", + "account_edit.image_alt_modal.add_title": "Añadir texto alternativo", + "account_edit.image_alt_modal.details_content": "QUE HACER:
  • Descríbete tal y como apareces en la imagen
  • Exprésate en tercera persona (p. ej. “Alex” en lugar de “yo”)
  • Sé breve, unas pocas palabras son a menudo suficientes
QUE NO HACER:
  • Comenzar con “Foto de” – es redundante para lectores de pantalla
EJEMPLO:
  • “Alex visitiendo una camisa verde y gafas”
", + "account_edit.image_alt_modal.details_title": "Consejo: Texto alternativo para fotos de perfil", + "account_edit.image_alt_modal.edit_title": "Editar texto alternativo", + "account_edit.image_alt_modal.text_hint": "El texto alternativo ayuda a los usuarios de lectores de pantalla a entender tu contenido.", + "account_edit.image_alt_modal.text_label": "Texto alternativo", + "account_edit.image_delete_modal.confirm": "¿Estás seguro de que deseas eliminar esta imagen? Esta acción no se puede deshacer.", + "account_edit.image_delete_modal.delete_button": "Eliminar", + "account_edit.image_delete_modal.title": "¿Eliminar imagen?", "account_edit.image_edit.add_button": "Añadir imagen", "account_edit.image_edit.alt_add_button": "Añadir texto alternativo", "account_edit.image_edit.alt_edit_button": "Editar texto alternativo", @@ -203,6 +211,16 @@ "account_edit.profile_tab.subtitle": "Personaliza las pestañas de tu perfil y lo que muestran.", "account_edit.profile_tab.title": "Configuración de la pestaña de perfil", "account_edit.save": "Guardar", + "account_edit.upload_modal.back": "Atrás", + "account_edit.upload_modal.done": "Hecho", + "account_edit.upload_modal.next": "Siguiente", + "account_edit.upload_modal.step_crop.zoom": "Zoom", + "account_edit.upload_modal.step_upload.button": "Explorar archivos", + "account_edit.upload_modal.step_upload.dragging": "Suelta para subir", + "account_edit.upload_modal.step_upload.header": "Elige una imagen", + "account_edit.upload_modal.step_upload.hint": "Formato WEBP, PNG, GIF o JPG, hasta {limit}MB.{br}La imagen será escalada a {width}x{height}px.", + "account_edit.upload_modal.title_add": "Añadir foto de perfil", + "account_edit.upload_modal.title_replace": "Reemplazar foto de perfil", "account_edit.verified_modal.details": "Añade credibilidad a tu perfil de Mastodon verificando enlaces a tus webs personales. Así es como funciona:", "account_edit.verified_modal.invisible_link.details": "Añade el enlace en el encabezado. La parte importante es rel=\"me\", que evita la suplantación de identidad en webs con contenido generado por usuarios. Incluso puedes utilizar un enlace con etiqueta en el encabezado de la página en vez de {tag}, pero el HTML debe ser accesible sin ejecutar JavaScript.", "account_edit.verified_modal.invisible_link.summary": "¿Cómo puedo hacer el enlace invisible?", @@ -368,7 +386,7 @@ "collections.remove_account": "Quitar esta cuenta", "collections.report_collection": "Informar de esta colección", "collections.revoke_collection_inclusion": "Sácame de esta colección", - "collections.revoke_inclusion.confirmation": "Has salido de la \"{collection}\"", + "collections.revoke_inclusion.confirmation": "Has salido de \"{collection}\"", "collections.revoke_inclusion.error": "Se ha producido un error, inténtalo de nuevo más tarde.", "collections.search_accounts_label": "Buscar cuentas para añadir…", "collections.search_accounts_max_reached": "Has añadido el número máximo de cuentas", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index d262f98c0e..13ba8bd303 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Vaata profiili", "account.hide_reblogs": "Peida @{name} jagamised", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "Liitus {date}", "account.joined_short": "Liitus", "account.languages": "Muuda tellitud keeli", "account.link_verified_on": "Selle lingi autorsust kontrolliti {date}", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index e8d5383cf4..60afb8591f 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Siirry profiiliin", "account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset", "account.in_memoriam": "Muistoissamme.", - "account.joined_long": "Liittynyt {date}", "account.joined_short": "Liittynyt", "account.languages": "Vaihda tilattuja kieliä", "account.link_verified_on": "Linkin omistus tarkistettiin {date}", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index d586fd5781..110cdd1e7f 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Far til vanga", "account.hide_reblogs": "Fjal stimbran frá @{name}", "account.in_memoriam": "In memoriam.", - "account.joined_long": "Meldaði til {date}", "account.joined_short": "Gjørdist limur", "account.languages": "Broyt fylgd mál", "account.link_verified_on": "Ognarskapur av hesum leinki var eftirkannaður {date}", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 802aed5bea..5073f5f40d 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Voir ce profil", "account.hide_reblogs": "Masquer les boosts de @{name}", "account.in_memoriam": "En souvenir de", - "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Inscrit·e", "account.languages": "Changer les langues abonnées", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -185,6 +184,14 @@ "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", "account_edit.field_reorder_modal.title": "Réorganiser les champs", "account_edit.image_alt_modal.add_title": "Ajouter un texte alternatif", + "account_edit.image_alt_modal.details_content": "À faire :
  • Se décrire comme vous apparaissez sur la photo
  • Utiliser la troisième personne (par exemple « Alex » au lieu de « moi »)
  • Être succinct·e – quelques mot suffisent souvent
À éviter :
  • Commencer par « Une photo de » – c'est redondant pour les lecteurs d'écran
Example :
  • « Alex portant une chemise vert et des lunettes »
", + "account_edit.image_alt_modal.details_title": "Astuces : texte alternatif pour les photos de profil", + "account_edit.image_alt_modal.edit_title": "Modifier le texte alternatif", + "account_edit.image_alt_modal.text_hint": "Le texte alternatif aide les personnes utilisant un lecteur d'écran à comprendre votre contenu.", + "account_edit.image_alt_modal.text_label": "Texte alternatif", + "account_edit.image_delete_modal.confirm": "Voulez-vous vraiment supprimer cette image ? Cette action est irréversible.", + "account_edit.image_delete_modal.delete_button": "Supprimer", + "account_edit.image_delete_modal.title": "Supprimer l'image ?", "account_edit.image_edit.add_button": "Ajouter une image", "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", @@ -206,6 +213,14 @@ "account_edit.save": "Enregistrer", "account_edit.upload_modal.back": "Retour", "account_edit.upload_modal.done": "Terminé", + "account_edit.upload_modal.next": "Suivant", + "account_edit.upload_modal.step_crop.zoom": "Agrandir", + "account_edit.upload_modal.step_upload.button": "Parcourir les fichiers", + "account_edit.upload_modal.step_upload.dragging": "Déposer pour téléverser", + "account_edit.upload_modal.step_upload.header": "Choisir une image", + "account_edit.upload_modal.step_upload.hint": "Format WebP, PNG, GIF ou JPEG, jusqu'à {limit} Mo.{br}L'image sera redimensionnée à {width} × {height} px.", + "account_edit.upload_modal.title_add": "Ajouter une photo de profil", + "account_edit.upload_modal.title_replace": "Remplacer la photo de profil", "account_edit.verified_modal.details": "Ajouter de la crédibilité à votre profil Mastodon en vérifiant les liens vers vos sites Web personnels. Voici comment cela fonctionne :", "account_edit.verified_modal.invisible_link.details": "Ajouter le lien dans votre en-tête. La partie importante est « rel=\"me\" » qui empêche l'usurpation d'identité sur des sites Web ayant du contenu généré par d'autres utilisateur·rice·s. Vous pouvez aussi utiliser une balise link dans l'en-tête de la page au lieu de {tag}, mais le code HTML doit être accessible sans avoir besoin d'exécuter du JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Comment rendre le lien invisible ?", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index 9947452dc8..8bd9c72c05 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Voir le profil", "account.hide_reblogs": "Masquer les partages de @{name}", "account.in_memoriam": "En mémoire.", - "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Ici depuis", "account.languages": "Modifier les langues d'abonnements", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -185,6 +184,14 @@ "account_edit.field_reorder_modal.handle_label": "Faites glisser le champ « {item} »", "account_edit.field_reorder_modal.title": "Réorganiser les champs", "account_edit.image_alt_modal.add_title": "Ajouter un texte alternatif", + "account_edit.image_alt_modal.details_content": "À faire :
  • Se décrire comme vous apparaissez sur la photo
  • Utiliser la troisième personne (par exemple « Alex » au lieu de « moi »)
  • Être succinct·e – quelques mot suffisent souvent
À éviter :
  • Commencer par « Une photo de » – c'est redondant pour les lecteurs d'écran
Example :
  • « Alex portant une chemise vert et des lunettes »
", + "account_edit.image_alt_modal.details_title": "Astuces : texte alternatif pour les photos de profil", + "account_edit.image_alt_modal.edit_title": "Modifier le texte alternatif", + "account_edit.image_alt_modal.text_hint": "Le texte alternatif aide les personnes utilisant un lecteur d'écran à comprendre votre contenu.", + "account_edit.image_alt_modal.text_label": "Texte alternatif", + "account_edit.image_delete_modal.confirm": "Voulez-vous vraiment supprimer cette image ? Cette action est irréversible.", + "account_edit.image_delete_modal.delete_button": "Supprimer", + "account_edit.image_delete_modal.title": "Supprimer l'image ?", "account_edit.image_edit.add_button": "Ajouter une image", "account_edit.image_edit.alt_add_button": "Ajouter un texte alternatif", "account_edit.image_edit.alt_edit_button": "Modifier le texte alternatif", @@ -206,6 +213,14 @@ "account_edit.save": "Enregistrer", "account_edit.upload_modal.back": "Retour", "account_edit.upload_modal.done": "Terminé", + "account_edit.upload_modal.next": "Suivant", + "account_edit.upload_modal.step_crop.zoom": "Agrandir", + "account_edit.upload_modal.step_upload.button": "Parcourir les fichiers", + "account_edit.upload_modal.step_upload.dragging": "Déposer pour téléverser", + "account_edit.upload_modal.step_upload.header": "Choisir une image", + "account_edit.upload_modal.step_upload.hint": "Format WebP, PNG, GIF ou JPEG, jusqu'à {limit} Mo.{br}L'image sera redimensionnée à {width} × {height} px.", + "account_edit.upload_modal.title_add": "Ajouter une photo de profil", + "account_edit.upload_modal.title_replace": "Remplacer la photo de profil", "account_edit.verified_modal.details": "Ajouter de la crédibilité à votre profil Mastodon en vérifiant les liens vers vos sites Web personnels. Voici comment cela fonctionne :", "account_edit.verified_modal.invisible_link.details": "Ajouter le lien dans votre en-tête. La partie importante est « rel=\"me\" » qui empêche l'usurpation d'identité sur des sites Web ayant du contenu généré par d'autres utilisateur·rice·s. Vous pouvez aussi utiliser une balise link dans l'en-tête de la page au lieu de {tag}, mais le code HTML doit être accessible sans avoir besoin d'exécuter du JavaScript.", "account_edit.verified_modal.invisible_link.summary": "Comment rendre le lien invisible ?", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 9e4dc96794..32f8e17412 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Téigh go dtí próifíl", "account.hide_reblogs": "Folaigh moltaí ó @{name}", "account.in_memoriam": "Ón tseanaimsir.", - "account.joined_long": "Chuaigh isteach ar {date}", "account.joined_short": "Cláraithe", "account.languages": "Athraigh teangacha foscríofa", "account.link_verified_on": "Seiceáladh úinéireacht an naisc seo ar {date}", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 330e7f832c..4b80a23a86 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Agochar promocións de @{name}", "account.in_memoriam": "Lembranzas.", - "account.joined_long": "Uníuse o {date}", "account.joined_short": "Uniuse", "account.languages": "Modificar os idiomas subscritos", "account.link_verified_on": "A propiedade desta ligazón foi verificada o {date}", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 27e5badca1..8adbb078d3 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -73,7 +73,6 @@ "account.go_to_profile": "מעבר לפרופיל", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", "account.in_memoriam": "פרופיל זכרון.", - "account.joined_long": "הצטרפו ב־{date}", "account.joined_short": "תאריך הצטרפות", "account.languages": "שנה רישום לשפות", "account.link_verified_on": "בעלות על הקישור הזה נבדקה לאחרונה ב{date}", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 7fcc82d1da..b9ff2185bd 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ugrás a profilhoz", "account.hide_reblogs": "@{name} megtolásainak elrejtése", "account.in_memoriam": "Emlékünkben.", - "account.joined_long": "Csatlakozás ideje: {date}", "account.joined_short": "Csatlakozott", "account.languages": "Feliratkozott nyelvek módosítása", "account.link_verified_on": "A linket eredetiségét ebben az időpontban ellenőriztük: {date}", @@ -163,7 +162,7 @@ "account_edit.featured_hashtags.item": "hashtagek", "account_edit.featured_hashtags.placeholder": "Segíts másoknak, hogy azonosíthassák a kedvenc témáid, és gyorsan elérjék azokat.", "account_edit.featured_hashtags.title": "Kiemelt hashtagek", - "account_edit.field_delete_modal.confirm": "Biztos, hogy töröld ezt az egyéni mezőt? Ez a művelet nem vonható vissza.", + "account_edit.field_delete_modal.confirm": "Biztos, hogy törlöd ezt az egyéni mezőt? Ez a művelet nem vonható vissza.", "account_edit.field_delete_modal.delete_button": "Törlés", "account_edit.field_delete_modal.title": "Egyéni mező törlése?", "account_edit.field_edit_modal.add_title": "Egyéni mező hozzáadása", @@ -184,6 +183,15 @@ "account_edit.field_reorder_modal.drag_start": "A(z) „{item}” mező áthelyezéshez felvéve.", "account_edit.field_reorder_modal.handle_label": "A(z) „{item}” mező húzása", "account_edit.field_reorder_modal.title": "Mezők átrendezése", + "account_edit.image_alt_modal.add_title": "Helyettesítő szöveg hozzáadása", + "account_edit.image_alt_modal.details_content": "TEDD:
  • Írd le a képedet
  • Használj egyes szám harmadik személyt (például „én” helyett „Alex”)
  • Légy tömör – sokszor néhány szó is elég
NE TEDD:
  • Ne kezdd azzal, hogy „X fényképe” – a képernyőolvasók számára felesleges
Példa:
  • „Alex zöld inget és szemüveget viselve”
", + "account_edit.image_alt_modal.details_title": "Tippek: helyettesítő szöveg a profilképekhez", + "account_edit.image_alt_modal.edit_title": "Helyettesítő szöveg szerkesztése", + "account_edit.image_alt_modal.text_hint": "A helyettesítő szövegek segítenek a képernyőolvasót használóknak abban, hogy megértsék a tartalmat.", + "account_edit.image_alt_modal.text_label": "Helyettesítő szöveg", + "account_edit.image_delete_modal.confirm": "Biztos, hogy törlöd ezt a képet? Ez a művelet nem vonható vissza.", + "account_edit.image_delete_modal.delete_button": "Törlés", + "account_edit.image_delete_modal.title": "Törlöd a képet?", "account_edit.image_edit.add_button": "Kép hozzáadása", "account_edit.image_edit.alt_add_button": "Helyettesítő szöveg hozzáadása", "account_edit.image_edit.alt_edit_button": "Helyettesítő szöveg szerkesztése", @@ -203,6 +211,15 @@ "account_edit.profile_tab.subtitle": "Szabd testre a profilodon látható lapokat, és a megjelenített tartalmukat.", "account_edit.profile_tab.title": "Profil lap beállításai", "account_edit.save": "Mentés", + "account_edit.upload_modal.back": "Vissza", + "account_edit.upload_modal.done": "Kész", + "account_edit.upload_modal.next": "Következő", + "account_edit.upload_modal.step_crop.zoom": "Nagyítás", + "account_edit.upload_modal.step_upload.button": "Fájlok tallózása", + "account_edit.upload_modal.step_upload.dragging": "Ejtsd ide a feltöltéshez", + "account_edit.upload_modal.step_upload.header": "Válassz egy képet", + "account_edit.upload_modal.title_add": "Profilkép hozzáadása", + "account_edit.upload_modal.title_replace": "Profilkép cseréje", "account_edit.verified_modal.details": "Növeld a Mastodon-profilod hitelességét a személyes webhelyekre mutató hivatkozások ellenőrzésével. Így működik:", "account_edit.verified_modal.invisible_link.details": "A hivatkozás hozzáadása a fejlécedhez. A fontos rész a rel=\"me\", mely megakadályozza, hogy mások a nevedben lépjenek fel olyan oldalakon, ahol van felhasználók által előállított tartalom. A(z) {tag} helyett a „link” címkét is használhatod az oldal fejlécében, de a HTML-nek elérhetőnek kell lennie JavaScript futtatása nélkül is.", "account_edit.verified_modal.invisible_link.summary": "Hogyan lehet egy hivatkozás láthatatlanná tenni?", @@ -374,6 +391,7 @@ "collections.search_accounts_max_reached": "Elérte a hozzáadott fiókok maximális számát", "collections.sensitive": "Érzékeny", "collections.topic_hint": "Egy hashtag hozzáadása segít másoknak abban, hogy megértsék a gyűjtemény fő témáját.", + "collections.topic_special_chars_hint": "A különleges karakterek mentéskor el lesznek távolítva", "collections.view_collection": "Gyűjtemény megtekintése", "collections.view_other_collections_by_user": "Felhasználó más gyűjteményeinek megtekintése", "collections.visibility_public": "Nyilvános", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index b6e192bb9f..442b8d60eb 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Fara í notandasnið", "account.hide_reblogs": "Fela endurbirtingar fyrir @{name}", "account.in_memoriam": "Minning.", - "account.joined_long": "Skáði sig {date}", "account.joined_short": "Gerðist þátttakandi", "account.languages": "Breyta tungumálum í áskrift", "account.link_verified_on": "Eignarhald á þessum tengli var athugað þann {date}", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 68560bed93..36fc7c4241 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Vai al profilo", "account.hide_reblogs": "Nascondi condivisioni da @{name}", "account.in_memoriam": "In memoria.", - "account.joined_long": "Su questa istanza dal {date}", "account.joined_short": "Iscritto", "account.languages": "Modifica le lingue d'iscrizione", "account.link_verified_on": "La proprietà di questo link è stata controllata il {date}", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 9689a99036..a3f09b7320 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -58,7 +58,6 @@ "account.follows_you": "Yeṭṭafaṛ-ik·em-id", "account.go_to_profile": "Ddu ɣer umaɣnu", "account.hide_reblogs": "Ffer ayen i ibeṭṭu @{name}", - "account.joined_long": "Yerna-d ass n {date}", "account.joined_short": "Izeddi da seg ass n", "account.languages": "Beddel tutlayin yettwajerden", "account.link_verified_on": "Taɣara n useɣwen-a tettwasenqed ass n {date}", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index 05b75de2f9..953fa1fb91 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -72,7 +72,6 @@ "account.go_to_profile": "프로필로 이동", "account.hide_reblogs": "@{name}의 부스트를 숨기기", "account.in_memoriam": "고인의 계정입니다.", - "account.joined_long": "{date}에 가입함", "account.joined_short": "가입", "account.languages": "구독한 언어 변경", "account.link_verified_on": "{date}에 이 링크의 소유권이 확인 됨", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index 32b23ea1e2..82a7eb9858 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -54,7 +54,7 @@ "account.follow": "Sekti", "account.follow_back": "Sekti atgal", "account.follow_back_short": "Sekti atgal", - "account.follow_request": "Prašyti sekti", + "account.follow_request": "Prašymas sekti", "account.follow_request_cancel": "Atšaukti prašymą", "account.follow_request_cancel_short": "Atšaukti", "account.follow_request_short": "Prašymas", @@ -69,7 +69,6 @@ "account.go_to_profile": "Eiti į profilį", "account.hide_reblogs": "Slėpti pasidalinimus iš @{name}", "account.in_memoriam": "Atminimui.", - "account.joined_long": "Prisijungė {date}", "account.joined_short": "Prisijungė", "account.languages": "Keisti prenumeruojamas kalbas", "account.link_verified_on": "Šios nuorodos nuosavybė buvo patikrinta {date}", diff --git a/app/javascript/mastodon/locales/nan-TW.json b/app/javascript/mastodon/locales/nan-TW.json index ce52b991e3..a463da983d 100644 --- a/app/javascript/mastodon/locales/nan-TW.json +++ b/app/javascript/mastodon/locales/nan-TW.json @@ -73,7 +73,6 @@ "account.go_to_profile": "行kàu個人資料", "account.hide_reblogs": "Tshàng tuì @{name} 來ê轉PO", "account.in_memoriam": "佇tsia追悼。", - "account.joined_long": "佇 {date} 加入", "account.joined_short": "加入ê時", "account.languages": "變更訂閱的語言", "account.link_verified_on": "Tsit ê連結ê所有權佇 {date} 受檢查", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index caaf831c8a..c998f6ac8c 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ga naar profiel", "account.hide_reblogs": "Boosts van @{name} verbergen", "account.in_memoriam": "In memoriam.", - "account.joined_long": "Geregistreerd op {date}", "account.joined_short": "Geregistreerd op", "account.languages": "Getoonde talen wijzigen", "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index e63555ce35..feb88565f8 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Gøym framhevingar frå @{name}", "account.in_memoriam": "Til minne om.", - "account.joined_long": "Vart med {date}", "account.joined_short": "Vart med", "account.languages": "Endre språktingingar", "account.link_verified_on": "Eigarskap for denne lenkja vart sist sjekka {date}", @@ -222,6 +221,13 @@ "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF eller JPG-format, opp til {limit}MB.{br}Biletet blir skalert til {width}*{height} punkt.", "account_edit.upload_modal.title_add": "Legg til profilbilete", "account_edit.upload_modal.title_replace": "Byt ut profilbilete", + "account_edit.verified_modal.details": "Auk truverdet til Mastodon-profilen din ved å stadfesta lenker til personlege nettstader. Slik verkar det:", + "account_edit.verified_modal.invisible_link.details": "Den viktige delen er rel=\"me\", som på nettstader med brukargenerert innhald vil hindra at andre kan låst som dei er deg. Du kan til og med bruka link i staden for {tag} i toppteksten til sida, men HTML-koden må vera tilgjengeleg utan å måtte køyra JavaScript.", + "account_edit.verified_modal.invisible_link.summary": "Korleis gjer eg lenka usynleg?", + "account_edit.verified_modal.step1.header": "Kopier HTML-koden under og lim han inn i toppfeltet på nettstaden din", + "account_edit.verified_modal.step2.details": "Viss du allereie har lagt til nettsida di i eit tilpassa felt, må du sletta ho og leggja ho til på nytt for å setja i gang stadfestinga.", + "account_edit.verified_modal.step2.header": "Legg til nettstaden din som eige felt", + "account_edit.verified_modal.title": "Korleis legg eg til ei stadfesta lenke", "account_edit_tags.add_tag": "Legg til #{tagName}", "account_edit_tags.column_title": "Rediger utvalde emneknaggar", "account_edit_tags.help_text": "Utvalde emneknaggar hjelper folk å oppdaga og samhandla med profilen din. Dei blir viste som filter på aktivitetsoversikta på profilsida di.", @@ -325,8 +331,8 @@ "callout.dismiss": "Avvis", "carousel.current": "Side {current, number} / {max, number}", "carousel.slide": "Side {current, number} av {max, number}", - "character_counter.recommended": "{currentLength}/{maxLength} anbefalte tegn", - "character_counter.required": "{currentLength}/{maxLength} tegn", + "character_counter.recommended": "{currentLength}/{maxLength} tilrådde teikn", + "character_counter.required": "{currentLength}/{maxLength} teikn", "closed_registrations.other_server_instructions": "Sidan Mastodon er desentralisert kan du lage ein brukar på ein anna tenar og framleis interagere med denne.", "closed_registrations_modal.description": "Det er ikkje mogleg å opprette ein konto på {domain} nett no, men hugs at du ikkje treng ein konto på akkurat {domain} for å nytte Mastodon.", "closed_registrations_modal.find_another_server": "Finn ein annan tenar", @@ -356,10 +362,15 @@ "collections.create_collection": "Lag ei samling", "collections.delete_collection": "Slett samlinga", "collections.description_length_hint": "Maks 100 teikn", + "collections.detail.accept_inclusion": "Ok", "collections.detail.accounts_heading": "Kontoar", + "collections.detail.author_added_you": "{author} la deg til i denne samlinga", "collections.detail.curated_by_author": "Kuratert av {author}", "collections.detail.curated_by_you": "Kuratert av deg", "collections.detail.loading": "Lastar inn samling…", + "collections.detail.other_accounts_in_collection": "Andre i denne samlinga:", + "collections.detail.revoke_inclusion": "Fjern meg", + "collections.detail.sensitive_note": "Denne samlinga inneheld kontoar og innhald som kan vera ømtolige for nokre menneske.", "collections.detail.share": "Del denne samlinga", "collections.edit_details": "Rediger detaljar", "collections.error_loading_collections": "Noko gjekk gale då me prøvde å henta samlingane dine.", @@ -374,10 +385,14 @@ "collections.old_last_post_note": "Sist lagt ut for over ei veke sidan", "collections.remove_account": "Fjern denne kontoen", "collections.report_collection": "Rapporter denne samlinga", + "collections.revoke_collection_inclusion": "Fjern meg frå denne samlinga", + "collections.revoke_inclusion.confirmation": "Du er fjerna frå «{collection}»", + "collections.revoke_inclusion.error": "Noko gjekk gale, prøv att seinare.", "collections.search_accounts_label": "Søk etter kontoar å leggja til…", "collections.search_accounts_max_reached": "Du har nådd grensa for kor mange kontoar du kan leggja til", "collections.sensitive": "Ømtolig", "collections.topic_hint": "Legg til ein emneknagg som hjelper andre å forstå hovudemnet for denne samlinga.", + "collections.topic_special_chars_hint": "Spesialteikn vil bli fjerna ved lagring", "collections.view_collection": "Sjå samlinga", "collections.view_other_collections_by_user": "Sjå andre samlingar frå denne personen", "collections.visibility_public": "Offentleg", @@ -498,7 +513,8 @@ "confirmations.remove_from_followers.message": "{name} vil ikkje fylgja deg meir. Vil du halda fram?", "confirmations.remove_from_followers.title": "Fjern fylgjar?", "confirmations.revoke_collection_inclusion.confirm": "Fjern meg", - "confirmations.revoke_collection_inclusion.title": "Fjern deg selv fra denne samlingen?", + "confirmations.revoke_collection_inclusion.message": "Denne handlinga er endeleg, og kuratoren kan ikkje leggja deg til samlinga på nytt seinare.", + "confirmations.revoke_collection_inclusion.title": "Vil du fjerna deg sjølv frå denne samlinga?", "confirmations.revoke_quote.confirm": "Fjern innlegget", "confirmations.revoke_quote.message": "Du kan ikkje angra denne handlinga.", "confirmations.revoke_quote.title": "Fjern innlegget?", @@ -821,6 +837,7 @@ "navigation_bar.automated_deletion": "Automatisk sletting av innlegg", "navigation_bar.blocks": "Blokkerte brukarar", "navigation_bar.bookmarks": "Bokmerke", + "navigation_bar.collections": "Samlingar", "navigation_bar.direct": "Private omtaler", "navigation_bar.domain_blocks": "Skjulte domene", "navigation_bar.favourites": "Favorittar", @@ -968,12 +985,14 @@ "notifications_permission_banner.title": "Gå aldri glipp av noko", "onboarding.follows.back": "Tilbake", "onboarding.follows.empty": "Me kan ikkje visa deg nokon resultat no. Du kan prøva å søkja eller bla gjennom utforsk-sida for å finna folk å fylgja, eller du kan prøva att seinare.", + "onboarding.follows.next": "Neste: Set opp profilen din", "onboarding.follows.search": "Søk", "onboarding.follows.title": "Fylg folk for å koma i gang", "onboarding.profile.discoverable": "Gjer profilen min synleg", "onboarding.profile.discoverable_hint": "Når du vel å gjera profilen din synleg på Mastodon, vil innlegga dine syna i søkjeresultat og populære innlegg, og profilen din kan bli føreslegen for folk med liknande interesser som deg.", "onboarding.profile.display_name": "Synleg namn", "onboarding.profile.display_name_hint": "Det fulle namnet eller kallenamnet ditt…", + "onboarding.profile.finish": "Fullfør", "onboarding.profile.note": "Om meg", "onboarding.profile.note_hint": "Du kan @nemna folk eller #emneknaggar…", "onboarding.profile.title": "Profiloppsett", diff --git a/app/javascript/mastodon/locales/no.json b/app/javascript/mastodon/locales/no.json index b0a6256b05..c1a1e33e84 100644 --- a/app/javascript/mastodon/locales/no.json +++ b/app/javascript/mastodon/locales/no.json @@ -71,7 +71,6 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul fremhevinger fra @{name}", "account.in_memoriam": "Til minne om.", - "account.joined_long": "Ble med den {date}", "account.joined_short": "Ble med", "account.languages": "Endre hvilke språk du abonnerer på", "account.link_verified_on": "Eierskap av denne lenken ble sjekket {date}", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 7fcfeb9053..68dd9d6f68 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -61,7 +61,6 @@ "account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ", "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ", "account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ", - "account.joined_long": "{date} ਨੂੰ ਜੁਆਇਨ ਕੀਤਾ", "account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ", "account.media": "ਮੀਡੀਆ", "account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 7b55c1e99d..88b21f47ca 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -57,7 +57,6 @@ "account.go_to_profile": "Przejdź do profilu", "account.hide_reblogs": "Ukryj podbicia od @{name}", "account.in_memoriam": "Ku pamięci.", - "account.joined_long": "Dołączył(a) dnia {date}", "account.joined_short": "Dołączył(a)", "account.languages": "Zmień subskrybowane języki", "account.link_verified_on": "Własność tego odnośnika została potwierdzona {date}", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index dc02337994..03e356313e 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "Entrou em {date}", "account.joined_short": "Entrou", "account.languages": "Mudar idiomas inscritos", "account.link_verified_on": "A propriedade deste link foi verificada em {date}", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index b1bb9de59b..d51565bb25 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Ir para o perfil", "account.hide_reblogs": "Esconder partilhas de @{name}", "account.in_memoriam": "Em Memória.", - "account.joined_long": "Juntou-se em {date}", "account.joined_short": "Juntou-se a", "account.languages": "Alterar idiomas subscritos", "account.link_verified_on": "O proprietário desta hiperligação foi verificado em {date}", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index bebc2b3998..b13ae55d10 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -55,7 +55,6 @@ "account.go_to_profile": "Pojdi na profil", "account.hide_reblogs": "Skrij izpostavitve od @{name}", "account.in_memoriam": "V spomin.", - "account.joined_long": "Pridružen/a {date}", "account.joined_short": "Pridružil/a", "account.languages": "Spremeni naročene jezike", "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 54f5513dae..ca7d378c1d 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Kalo te profili", "account.hide_reblogs": "Fshih përforcime nga @{name}", "account.in_memoriam": "In Memoriam.", - "account.joined_long": "U bë pjesë më {date}", "account.joined_short": "U bë pjesë", "account.languages": "Ndryshoni gjuhë pajtimesh", "account.link_verified_on": "Pronësia e kësaj lidhjeje qe kontrolluar më {date}", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 7997992887..4c781e24a5 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Gå till profilen", "account.hide_reblogs": "Dölj boostar från @{name}", "account.in_memoriam": "Till minne av.", - "account.joined_long": "Gick med {date}", "account.joined_short": "Gick med", "account.languages": "Ändra vilka språk du helst vill se i ditt flöde", "account.link_verified_on": "Ägarskap för denna länk kontrollerades den {date}", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index a02425a27b..81140ef57d 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Profile git", "account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle", "account.in_memoriam": "Hatırasına.", - "account.joined_long": "{date} tarihinde katıldı", "account.joined_short": "Katıldı", "account.languages": "Abone olunan dilleri değiştir", "account.link_verified_on": "Bu bağlantının sahipliği {date} tarihinde denetlendi", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 65522d62d7..67fe49725a 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -68,7 +68,6 @@ "account.go_to_profile": "Перейти до профілю", "account.hide_reblogs": "Сховати поширення від @{name}", "account.in_memoriam": "Пам'ятник.", - "account.joined_long": "Долучилися {date}", "account.joined_short": "Дата приєднання", "account.languages": "Змінити обрані мови", "account.link_verified_on": "Права власності на це посилання були перевірені {date}", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 95b35aec0a..186da4c87f 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -73,7 +73,6 @@ "account.go_to_profile": "Xem hồ sơ", "account.hide_reblogs": "Ẩn tút @{name} đăng lại", "account.in_memoriam": "Tưởng Niệm.", - "account.joined_long": "Tham gia {date}", "account.joined_short": "Tham gia", "account.languages": "Đổi ngôn ngữ mong muốn", "account.link_verified_on": "Liên kết này đã được xác minh vào {date}", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 92b260542b..c4c162124b 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -73,7 +73,6 @@ "account.go_to_profile": "前往个人资料页", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.in_memoriam": "谨此悼念。", - "account.joined_long": "加入于 {date}", "account.joined_short": "加入于", "account.languages": "更改订阅语言", "account.link_verified_on": "此链接的所有权已在 {date} 检查", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 73b2649aac..eb1dd18fe8 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -73,7 +73,6 @@ "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏來自 @{name} 之轉嘟", "account.in_memoriam": "謹此悼念。", - "account.joined_long": "加入於 {date}", "account.joined_short": "加入時間", "account.languages": "變更訂閱的語言", "account.link_verified_on": "已於 {date} 檢查此連結的擁有者權限", diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index c8109fffb5..6248d8e97b 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -82,6 +82,9 @@ export const accountDefaultValues: AccountShape = { last_status_at: '', locked: false, noindex: false, + show_featured: true, + show_media: true, + show_media_replies: true, note: '', note_emojified: '', note_plain: 'string', diff --git a/app/javascript/mastodon/reducers/slices/profile_edit.ts b/app/javascript/mastodon/reducers/slices/profile_edit.ts index aa1ae4a73a..9148a55f5f 100644 --- a/app/javascript/mastodon/reducers/slices/profile_edit.ts +++ b/app/javascript/mastodon/reducers/slices/profile_edit.ts @@ -1,8 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { debounce } from 'lodash'; - import { fetchAccount } from '@/mastodon/actions/accounts'; import { apiDeleteFeaturedTag, @@ -14,7 +11,6 @@ import { apiPatchProfile, apiPostFeaturedTag, } from '@/mastodon/api/accounts'; -import { apiGetSearch } from '@/mastodon/api/search'; import type { ApiAccountFieldJSON } from '@/mastodon/api_types/accounts'; import type { ApiProfileJSON, @@ -24,7 +20,6 @@ import type { ApiFeaturedTagJSON, ApiHashtagJSON, } from '@/mastodon/api_types/tags'; -import type { AppDispatch } from '@/mastodon/store'; import { createAppAsyncThunk, createAppSelector, @@ -59,40 +54,16 @@ export interface ProfileEditState { profile?: ProfileData; tagSuggestions?: ApiHashtagJSON[]; isPending: boolean; - search: { - query: string; - isLoading: boolean; - results?: ApiHashtagJSON[]; - }; } const initialState: ProfileEditState = { isPending: false, - search: { - query: '', - isLoading: false, - }, }; const profileEditSlice = createSlice({ name: 'profileEdit', initialState, - reducers: { - setSearchQuery(state, action: PayloadAction) { - if (state.search.query === action.payload) { - return; - } - - state.search.query = action.payload; - state.search.isLoading = true; - state.search.results = undefined; - }, - clearSearch(state) { - state.search.query = ''; - state.search.isLoading = false; - state.search.results = undefined; - }, - }, + reducers: {}, extraReducers(builder) { builder.addCase(fetchProfile.fulfilled, (state, action) => { state.profile = action.payload; @@ -172,37 +143,10 @@ const profileEditSlice = createSlice({ ); state.isPending = false; }); - - builder.addCase(fetchSearchResults.pending, (state) => { - state.search.isLoading = true; - }); - builder.addCase(fetchSearchResults.rejected, (state) => { - state.search.isLoading = false; - state.search.results = undefined; - }); - builder.addCase(fetchSearchResults.fulfilled, (state, action) => { - state.search.isLoading = false; - const searchResults: ApiHashtagJSON[] = []; - const currentTags = new Set( - (state.profile?.featuredTags ?? []).map((tag) => tag.name), - ); - - for (const tag of action.payload) { - if (currentTags.has(tag.name)) { - continue; - } - searchResults.push(tag); - if (searchResults.length >= 10) { - break; - } - } - state.search.results = searchResults; - }); }, }); export const profileEdit = profileEditSlice.reducer; -export const { clearSearch } = profileEditSlice.actions; const transformTag = (result: ApiFeaturedTagJSON): TagData => ({ id: result.id, @@ -426,27 +370,3 @@ export const deleteFeaturedTag = createDataLoadingThunk( `${profileEditSlice.name}/deleteFeaturedTag`, ({ tagId }: { tagId: string }) => apiDeleteFeaturedTag(tagId), ); - -const debouncedFetchSearchResults = debounce( - async (dispatch: AppDispatch, query: string) => { - await dispatch(fetchSearchResults({ q: query })); - }, - 300, -); - -export const updateSearchQuery = createAppAsyncThunk( - `${profileEditSlice.name}/updateSearchQuery`, - (query: string, { dispatch }) => { - dispatch(profileEditSlice.actions.setSearchQuery(query)); - - if (query.trim().length > 0) { - void debouncedFetchSearchResults(dispatch, query); - } - }, -); - -export const fetchSearchResults = createDataLoadingThunk( - `${profileEditSlice.name}/fetchSearchResults`, - ({ q }: { q: string }) => apiGetSearch({ q, type: 'hashtags', limit: 11 }), - (result) => result.hashtags, -); diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index ae9ea34582..add5617930 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -28,6 +28,7 @@ import { } from '../actions/timelines_typed'; import { compareId } from '../compare_id'; +/** @type {ImmutableMap} */ const initialState = ImmutableMap(); const initialTimeline = ImmutableMap({ @@ -36,7 +37,9 @@ const initialTimeline = ImmutableMap({ top: true, isLoading: false, hasMore: true, + /** @type {ImmutableList} */ pendingItems: ImmutableList(), + /** @type {ImmutableList} */ items: ImmutableList(), }); @@ -197,6 +200,7 @@ const reconnectTimeline = (state, usePendingItems) => { }); }; +/** @type {import('@reduxjs/toolkit').Reducer} */ export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_LOAD_PENDING: diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 58421817ad..5f736fa80c 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -18,7 +18,7 @@ export function isServerFeatureEnabled(feature: ServerFeatures) { return initialState?.features.includes(feature) ?? false; } -type ClientFeatures = 'collections' | 'profile_editing'; +type ClientFeatures = 'collections'; export function isClientFeatureEnabled(feature: ClientFeatures) { try { diff --git a/app/javascript/mastodon/utils/hashtags.ts b/app/javascript/mastodon/utils/hashtags.ts index ff90a88465..963ca48369 100644 --- a/app/javascript/mastodon/utils/hashtags.ts +++ b/app/javascript/mastodon/utils/hashtags.ts @@ -28,6 +28,12 @@ export const HASHTAG_PATTERN_REGEX = buildHashtagPatternRegex(); export const HASHTAG_REGEX = buildHashtagRegex(); +export const trimHashFromStart = (input: string) => { + return input.startsWith('#') || input.startsWith('#') + ? input.slice(1) + : input; +}; + /** * Formats an input string as a hashtag: * - Prepends `#` unless present @@ -41,11 +47,7 @@ export const inputToHashtag = (input: string): string => { const trailingSpace = /\s+$/.exec(input)?.[0] ?? ''; const trimmedInput = input.trimEnd(); - - const withoutHash = - trimmedInput.startsWith('#') || trimmedInput.startsWith('#') - ? trimmedInput.slice(1) - : trimmedInput; + const withoutHash = trimHashFromStart(trimmedInput); // Split by space, filter empty strings, and capitalise the start of each word but the first const words = withoutHash diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 1efd22c3c9..e345e08351 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -45,6 +45,9 @@ export const accountFactory: FactoryFunction = ({ indexable: true, last_status_at: '2023-01-01', locked: false, + show_featured: true, + show_media: true, + show_media_replies: true, mute_expires_at: null, note: 'This is a test user account.', statuses_count: 0, diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index f606d9520f..62c298a638 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -2,11 +2,10 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity def perform - if @account.uri == object_uri - delete_person - else - delete_object - end + return delete_person if @account.uri == object_uri + return delete_feature_authorization! unless !Mastodon::Feature.collections_federation_enabled? || feature_authorization_from_object.nil? + + delete_object end private @@ -66,7 +65,18 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity DistributionWorker.perform_async(@quote.status_id, { 'update' => true }) if @quote.status.present? end + def delete_feature_authorization! + collection_item = feature_authorization_from_object + DeleteCollectionItemService.new.call(collection_item, revoke: true) + end + def forwarder @forwarder ||= ActivityPub::Forwarder.new(@account, @json, @status) end + + def feature_authorization_from_object + return @collection_item if instance_variable_defined?(:@collection_item) + + @collection_item = CollectionItem.local.find_by(approval_uri: value_or_id(@object), account_id: @account.id) + end end diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb index 25140598a5..b13c5c97d4 100644 --- a/app/models/web/push_subscription.rb +++ b/app/models/web/push_subscription.rb @@ -29,6 +29,7 @@ class Web::PushSubscription < ApplicationRecord validates_with WebPushKeyValidator delegate :locale, to: :user + delegate :token, to: :access_token, prefix: :associated_access generates_token_for :unsubscribe, expires_in: Web::PushNotificationWorker::TTL @@ -36,10 +37,6 @@ class Web::PushSubscription < ApplicationRecord policy_allows_notification?(notification) && alert_enabled_for_notification_type?(notification) end - def associated_access_token - access_token.token - end - class << self def unsubscribe_for(application_id, resource_owner) access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id).not_revoked.pluck(:id) diff --git a/app/services/activitypub/fetch_featured_collections_collection_service.rb b/app/services/activitypub/fetch_featured_collections_collection_service.rb new file mode 100644 index 0000000000..99a45fac3e --- /dev/null +++ b/app/services/activitypub/fetch_featured_collections_collection_service.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ActivityPub::FetchFeaturedCollectionsCollectionService < BaseService + include JsonLdHelper + + MAX_PAGES = 10 + MAX_ITEMS = 50 + + def call(account, request_id: nil) + return if account.collections_url.blank? || account.suspended? || account.local? + + @request_id = request_id + @account = account + @items, = collection_items(@account.collections_url, max_pages: MAX_PAGES, reference_uri: @account.uri) + process_items(@items) + end + + private + + def process_items(items) + return if items.nil? + + items.take(MAX_ITEMS).each do |collection_json| + if collection_json.is_a?(String) + ActivityPub::FetchRemoteFeaturedCollectionService.new.call(collection_json, request_id: @request_id) + else + ActivityPub::ProcessFeaturedCollectionService.new.call(@account, collection_json, request_id: @request_id) + end + end + end +end diff --git a/app/services/activitypub/fetch_remote_featured_collection_service.rb b/app/services/activitypub/fetch_remote_featured_collection_service.rb index e9858fe34d..babad143e1 100644 --- a/app/services/activitypub/fetch_remote_featured_collection_service.rb +++ b/app/services/activitypub/fetch_remote_featured_collection_service.rb @@ -3,7 +3,7 @@ class ActivityPub::FetchRemoteFeaturedCollectionService < BaseService include JsonLdHelper - def call(uri, on_behalf_of = nil) + def call(uri, request_id: nil, on_behalf_of: nil) json = fetch_resource(uri, true, on_behalf_of) return unless supported_context?(json) @@ -17,6 +17,6 @@ class ActivityPub::FetchRemoteFeaturedCollectionService < BaseService existing_collection = account.collections.find_by(uri:) return existing_collection if existing_collection.present? - ActivityPub::ProcessFeaturedCollectionService.new.call(account, json) + ActivityPub::ProcessFeaturedCollectionService.new.call(account, json, request_id:) end end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index fb26992bf4..dd0eeadaa0 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -60,6 +60,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_federation_enabled? check_links! if @account.fields.any?(&:requires_verification?) end @@ -201,6 +202,10 @@ class ActivityPub::ProcessAccountService < BaseService ActivityPub::SynchronizeFeaturedTagsCollectionWorker.perform_async(@account.id, @json['featuredTags']) end + def check_featured_collections_collection! + ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker.perform_async(@account.id, @options[:request_id]) + end + def check_links! VerifyAccountLinksWorker.perform_in(rand(10.minutes.to_i), @account.id) end diff --git a/app/services/activitypub/process_featured_collection_service.rb b/app/services/activitypub/process_featured_collection_service.rb index 73db0d6699..91c15bdce2 100644 --- a/app/services/activitypub/process_featured_collection_service.rb +++ b/app/services/activitypub/process_featured_collection_service.rb @@ -7,9 +7,10 @@ class ActivityPub::ProcessFeaturedCollectionService ITEMS_LIMIT = 150 - def call(account, json) + def call(account, json, request_id: nil) @account = account @json = json + @request_id = request_id return if non_matching_uri_hosts?(@account.uri, @json['id']) with_redis_lock("collection:#{@json['id']}") do @@ -46,7 +47,7 @@ class ActivityPub::ProcessFeaturedCollectionService def process_items! @json['orderedItems'].take(ITEMS_LIMIT).each do |item_json| - ActivityPub::ProcessFeaturedItemWorker.perform_async(@collection.id, item_json) + ActivityPub::ProcessFeaturedItemWorker.perform_async(@collection.id, item_json, @request_id) end end end diff --git a/app/services/activitypub/process_featured_item_service.rb b/app/services/activitypub/process_featured_item_service.rb index b69cfc54e8..c0d2bfd206 100644 --- a/app/services/activitypub/process_featured_item_service.rb +++ b/app/services/activitypub/process_featured_item_service.rb @@ -5,7 +5,8 @@ class ActivityPub::ProcessFeaturedItemService include Lockable include Redisable - def call(collection, uri_or_object) + def call(collection, uri_or_object, request_id: nil) + @request_id = request_id item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object return if non_matching_uri_hosts?(collection.uri, item_json['id']) @@ -35,8 +36,8 @@ class ActivityPub::ProcessFeaturedItemService private def verify_authorization! - ActivityPub::VerifyFeaturedItemService.new.call(@collection_item, @approval_uri) + ActivityPub::VerifyFeaturedItemService.new.call(@collection_item, @approval_uri, request_id: @request_id) rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS - ActivityPub::VerifyFeaturedItemWorker.perform_in(rand(30..600).seconds, @collection_item.id, @approval_uri) + ActivityPub::VerifyFeaturedItemWorker.perform_in(rand(30..600).seconds, @collection_item.id, @approval_uri, @request_id) end end diff --git a/app/services/activitypub/verify_featured_item_service.rb b/app/services/activitypub/verify_featured_item_service.rb index ecebdf2075..6ce524870c 100644 --- a/app/services/activitypub/verify_featured_item_service.rb +++ b/app/services/activitypub/verify_featured_item_service.rb @@ -3,7 +3,7 @@ class ActivityPub::VerifyFeaturedItemService include JsonLdHelper - def call(collection_item, approval_uri) + def call(collection_item, approval_uri, request_id: nil) @collection_item = collection_item @authorization = fetch_resource(approval_uri, true, raise_on_error: :temporary) @@ -16,7 +16,7 @@ class ActivityPub::VerifyFeaturedItemService return unless matching_type? && matching_collection_uri? account = Account.where(uri: @collection_item.object_uri).first - account ||= ActivityPub::FetchRemoteAccountService.new.call(@collection_item.object_uri) + account ||= ActivityPub::FetchRemoteAccountService.new.call(@collection_item.object_uri, request_id:) return if account.blank? @collection_item.update!(account:, approval_uri:, state: :accepted) diff --git a/app/services/delete_collection_item_service.rb b/app/services/delete_collection_item_service.rb index 23f539a685..47df001d60 100644 --- a/app/services/delete_collection_item_service.rb +++ b/app/services/delete_collection_item_service.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true class DeleteCollectionItemService - def call(collection_item) + def call(collection_item, revoke: false) @collection_item = collection_item @collection = collection_item.collection - @collection_item.destroy! + + revoke ? @collection_item.revoke! : @collection_item.destroy! distribute_remove_activity if Mastodon::Feature.collections_federation_enabled? end diff --git a/app/services/report_service.rb b/app/services/report_service.rb index 3e418dc85a..9366a69e75 100644 --- a/app/services/report_service.rb +++ b/app/services/report_service.rb @@ -88,11 +88,18 @@ class ReportService < BaseService has_followers = @target_account.followers.with_domain(domain).exists? visibility = has_followers ? %i(public unlisted private) : %i(public unlisted) scope = @target_account.statuses.with_discarded - scope.merge!(scope.where(visibility: visibility).or(scope.where('EXISTS (SELECT 1 FROM mentions m JOIN accounts a ON m.account_id = a.id WHERE lower(a.domain) = ?)', domain))) + scope.merge!(scope.where(visibility: visibility).or(scope.where(domain_mentions(domain)))) # Allow missing posts to not drop reports that include e.g. a deleted post scope.where(id: Array(@status_ids)).pluck(:id) end + def domain_mentions(domain) + Mention + .joins(:account) + .where(Account.arel_table[:domain].lower.eq domain) + .select(1).arel.exists + end + def reported_collection_ids @target_account.collections.find(Array(@collection_ids)).pluck(:id) end diff --git a/app/workers/activitypub/process_featured_item_worker.rb b/app/workers/activitypub/process_featured_item_worker.rb index dd765e7df6..f50ff50b40 100644 --- a/app/workers/activitypub/process_featured_item_worker.rb +++ b/app/workers/activitypub/process_featured_item_worker.rb @@ -6,10 +6,10 @@ class ActivityPub::ProcessFeaturedItemWorker sidekiq_options queue: 'pull', retry: 3 - def perform(collection_id, id_or_json) + def perform(collection_id, id_or_json, request_id = nil) collection = Collection.find(collection_id) - ActivityPub::ProcessFeaturedItemService.new.call(collection, id_or_json) + ActivityPub::ProcessFeaturedItemService.new.call(collection, id_or_json, request_id:) rescue ActiveRecord::RecordNotFound true end diff --git a/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb b/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb new file mode 100644 index 0000000000..e24fe59b07 --- /dev/null +++ b/app/workers/activitypub/synchronize_featured_collections_collection_worker.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker + include Sidekiq::Worker + + sidekiq_options queue: 'pull', lock: :until_executed, lock_ttl: 1.day.to_i + + def perform(account_id, request_id = nil) + account = Account.find(account_id) + + ActivityPub::FetchFeaturedCollectionsCollectionService.new.call(account, request_id:) + rescue ActiveRecord::RecordNotFound + true + end +end diff --git a/app/workers/activitypub/verify_featured_item_worker.rb b/app/workers/activitypub/verify_featured_item_worker.rb index cd47f5ee21..3e96b81c09 100644 --- a/app/workers/activitypub/verify_featured_item_worker.rb +++ b/app/workers/activitypub/verify_featured_item_worker.rb @@ -7,10 +7,10 @@ class ActivityPub::VerifyFeaturedItemWorker sidekiq_options queue: 'pull', retry: 5 - def perform(collection_item_id, approval_uri) + def perform(collection_item_id, approval_uri, request_id = nil) collection_item = CollectionItem.find(collection_item_id) - ActivityPub::VerifyFeaturedItemService.new.call(collection_item, approval_uri) + ActivityPub::VerifyFeaturedItemService.new.call(collection_item, approval_uri, request_id:) rescue ActiveRecord::RecordNotFound # Do nothing nil diff --git a/config/locales/be.yml b/config/locales/be.yml index 425d3e606f..9d06a26030 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -810,6 +810,8 @@ be: administrator_description: Карыстальнікі з гэтым дазволам будуць абыходзіць усе абмежаванні delete_user_data: Выдаленне даных карыстальнікаў delete_user_data_description: Дазваляе карыстальнікам без затрымкі выдаляць даныя іншых карыстальнікаў + invite_bypass_approval: Запрашаць карыстальнікаў без разгляду + invite_bypass_approval_description: Дазваляе людзям, якіх запрасілі на сервер гэтыя карыстальнікі, абыходзіць ухвалу мадэратараў invite_users: Запрашэнне карыстальнікаў invite_users_description: Дазваляе запрашаць новых людзей на сервер manage_announcements: Кіраванне аб’явамі @@ -1337,6 +1339,7 @@ be: invited_by: 'Вы можаце далучыцца да %{domain} дзякуючы запрашэнню, якое вы атрымалі ад:' preamble: Правілы вызначаныя мадэратарамі дамена %{domain}. preamble_invited: Перш чым працягнуць, азнаёмцеся з асноўнымі правіламі, усталяванымі мадэратарамі %{domain}. + read_more: Падрабязней title: Некалькі базавых правілаў. title_invited: Вас запрасілі. security: Бяспека diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 46843c122d..560bc3e6f3 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1363,6 +1363,7 @@ cy: progress: confirm: Cadarnhau'r e-bost details: Eich manylion + list: Y drefn cofrestru review: Ein hadolygiad rules: Derbyn rheolau providers: @@ -1378,6 +1379,7 @@ cy: invited_by: 'Gallwch ymuno â %{domain} diolch i''r gwahoddiad a gawsoch gan:' preamble: Mae'r rhain yn cael eu gosod a'u gorfodi gan y %{domain} cymedrolwyr. preamble_invited: Cyn i chi barhau, ystyriwch y rheolau sylfaenol a osodwyd gan gymedrolwyr %{domain}. + read_more: Darllen rhagor title: Rhai rheolau sylfaenol. title_invited: Rydych wedi derbyn gwahoddiad. security: Diogelwch diff --git a/config/locales/da.yml b/config/locales/da.yml index d12d68a6d0..c83750d58c 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -778,6 +778,8 @@ da: administrator_description: Brugere med denne rolle kan omgå alle tilladelser delete_user_data: Slet brugerdata delete_user_data_description: Tillader brugere at slette andre brugeres data straks + invite_bypass_approval: Invitere brugere uden gennemgang + invite_bypass_approval_description: Gør det muligt for personer, der er inviteret til serveren af disse brugere, at undgå moderatorgodkendelse invite_users: Invitér brugere invite_users_description: Tillader brugere at invitere nye personer til serveren manage_announcements: Administrer annonceringer diff --git a/config/locales/de.yml b/config/locales/de.yml index f981cff223..4981257442 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -778,6 +778,8 @@ de: administrator_description: Beschränkung aller Berechtigungen umgehen delete_user_data: Kontodaten löschen delete_user_data_description: Daten anderer Profile ohne Verzögerung löschen + invite_bypass_approval: Einladungen ohne Überprüfungen + invite_bypass_approval_description: Neue Benutzer*innen zur Registrierung auf diesem Server einladen, ohne manuell genehmigt werden zu müssen invite_users: Einladungen invite_users_description: Neue Benutzer*innen zur Registrierung auf diesem Server einladen manage_announcements: Ankündigungen diff --git a/config/locales/el.yml b/config/locales/el.yml index b0619d1521..de95a5b725 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -778,6 +778,8 @@ el: administrator_description: Οι χρήστες με αυτό το δικαίωμα θα παρακάμπτουν κάθε δικαίωμα delete_user_data: Διαγραφή Δεδομένων Χρήστη delete_user_data_description: Επιτρέπει στους χρήστες να διαγράφουν τα δεδομένα άλλων χρηστών χωρίς καθυστέρηση + invite_bypass_approval: Πρόσκληση Χρηστών χωρίς έλεγχο + invite_bypass_approval_description: Επιτρέπει σε άτομα που προσκαλούνται στον διακομιστή από αυτούς τους χρήστες, να παρακάμψουν την έγκριση από συντονιστές invite_users: Πρόσκληση Χρηστών invite_users_description: Επιτρέπει στους χρήστες να προσκαλούν νέα άτομα στον διακομιστή manage_announcements: Διαχείριση Ανακοινώσεων diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index e168affc4a..e4c8e85622 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -778,6 +778,8 @@ es-AR: administrator_description: Los usuarios con este permiso saltarán todos los permisos delete_user_data: Eliminar datos del usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar a usuarios sin revisión + invite_bypass_approval_description: Permite —a las personas invitadas al servidor por estos usuarios— eludir la aprobación de moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar anuncios diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index 1710a61fe8..22e8afa757 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -778,6 +778,8 @@ es-MX: administrator_description: Los usuarios con este permiso saltarán todos los permisos delete_user_data: Borrar Datos de Usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar a usuarios sin revisión + invite_bypass_approval_description: Permite que las personas invitadas al servidor por estos usuarios no tengan que pasar por el proceso de aprobación de la moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar Anuncios @@ -1295,6 +1297,7 @@ es-MX: invited_by: 'Puedes unirte a %{domain} gracias a la invitación que has recibido de:' preamble: Estas son establecidas y aplicadas por los moderadores de %{domain}. preamble_invited: Antes de continuar, por favor, revisa las reglas básicas establecidas por los moderadores de %{domain}. + read_more: Leer más title: Algunas reglas básicas. title_invited: Has sido invitado. security: Cambiar contraseña diff --git a/config/locales/es.yml b/config/locales/es.yml index b5014dba7e..c7de434673 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -778,6 +778,8 @@ es: administrator_description: Los usuarios con este permiso saltarán todos los permisos delete_user_data: Borrar Datos de Usuario delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora + invite_bypass_approval: Invitar usuarios sin revisión + invite_bypass_approval_description: Permite registrarse a las personas invitadas al servidor por estos usuarios sin la aprobación de la moderación invite_users: Invitar usuarios invite_users_description: Permite a los usuarios invitar a nuevas personas al servidor manage_announcements: Administrar Anuncios @@ -1279,6 +1281,7 @@ es: progress: confirm: Confirmar dirección de correo details: Tus detalles + list: Progreso de registro review: Nuestra revisión rules: Aceptar reglas providers: @@ -1294,6 +1297,7 @@ es: invited_by: 'Puedes unirte a %{domain} gracias a la invitación que has recibido de:' preamble: Estas son establecidas y aplicadas por los moderadores de %{domain}. preamble_invited: Antes de continuar, por favor, revisa las reglas básicas establecidas por los moderadores de %{domain}. + read_more: Leer más title: Algunas reglas básicas. title_invited: Has sido invitado. security: Cambiar contraseña diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 551bb1ffa3..49e6a46582 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1295,6 +1295,7 @@ fi: invited_by: 'Voit liittyä palvelimelle %{domain} kutsulla, jonka sait seuraavalta käyttäjältä:' preamble: Palvelimen %{domain} moderaattorit määrittävät ja valvovat sääntöjä. preamble_invited: Ennen kuin jatkat ota huomioon palvelimen %{domain} moderaattorien asettamat perussäännöt. + read_more: Lue lisää title: Joitakin perussääntöjä. title_invited: Sinut on kutsuttu. security: Turvallisuus diff --git a/config/locales/fr-CA.yml b/config/locales/fr-CA.yml index 33e33fae6b..a1277b08ae 100644 --- a/config/locales/fr-CA.yml +++ b/config/locales/fr-CA.yml @@ -1298,6 +1298,7 @@ fr-CA: invited_by: 'Vous pouvez rejoindre %{domain} grâve à l''invitation reçue de:' preamble: Celles-ci sont définies et appliqués par les modérateurs de %{domain}. preamble_invited: Avant de continuer, veuillez lire les règles de base définies par les modérateurs de %{domain}. + read_more: Lire plus title: Quelques règles de base. title_invited: Vous avez été invité·e. security: Sécurité diff --git a/config/locales/fr.yml b/config/locales/fr.yml index c1008137cb..f65368ce14 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1298,6 +1298,7 @@ fr: invited_by: 'Vous pouvez rejoindre %{domain} grâce à l''invitation de :' preamble: Celles-ci sont définies et appliqués par les modérateurs de %{domain}. preamble_invited: Avant de continuer, veuillez lire les règles de base définies par les modérateurs de %{domain}. + read_more: Lire plus title: Quelques règles de base. title_invited: Vous avez été invité·e. security: Sécurité diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 802ae59001..1295a4efc5 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -826,6 +826,8 @@ ga: administrator_description: Seachnóidh úsáideoirí a bhfuil an cead seo acu gach cead delete_user_data: Scrios Sonraí Úsáideora delete_user_data_description: Ligeann sé d'úsáideoirí sonraí úsáideoirí eile a scriosadh gan mhoill + invite_bypass_approval: Tabhair cuireadh d'úsáideoirí gan athbhreithniú + invite_bypass_approval_description: Ceadaíonn sé do dhaoine a bhfuil cuireadh tugtha dóibh chuig an bhfreastalaí ag na húsáideoirí seo ceadú modhnóireachta a sheachaint invite_users: Tabhair cuireadh d'Úsáideoirí invite_users_description: Ligeann sé d'úsáideoirí cuireadh a thabhairt do dhaoine nua chuig an bhfreastalaí manage_announcements: Bainistigh Fógraí @@ -1360,6 +1362,7 @@ ga: invited_by: 'Is féidir leat páirt a ghlacadh i %{domain} a bhuíochas leis an gcuireadh a fuair tú ó:' preamble: Socraíonn agus cuireann na modhnóirí %{domain} iad seo i bhfeidhm. preamble_invited: Sula dtéann tú ar aghaidh, smaoinigh le do thoil ar na bunrialacha atá socraithe ag modhnóirí %{domain}. + read_more: Léigh tuilleadh title: Roinnt bunrialacha. title_invited: Tá cuireadh faighte agat. security: Slándáil diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 115b9ab3df..469f7badd7 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -778,6 +778,8 @@ gl: administrator_description: As usuarias con este permiso poderán superar calquera restrición delete_user_data: Eliminar datos de usuarias delete_user_data_description: Permite eliminar datos doutras usuarias sen demoras + invite_bypass_approval: Invitar sen precisar revisión + invite_bypass_approval_description: Permitir que as persoas invitadas ao servidor por estas usuarias non precisen aprobación invite_users: Convidar usuarias invite_users_description: Permite que outras usuarias conviden a xente ao servidor manage_announcements: Xestionar anuncios diff --git a/config/locales/he.yml b/config/locales/he.yml index b473c34a81..d18d44689d 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -810,6 +810,8 @@ he: administrator_description: משתמשים עם הרשאה זו יוכלו לעקוף כל הרשאה delete_user_data: מחיקת כל נתוני המשתמש delete_user_data_description: מאפשר למשתמשים למחוק נתוני משתמשים אחרים ללא דיחוי + invite_bypass_approval: הזמנה להרשם ללא הליך בדיקה + invite_bypass_approval_description: להרשות למשתמשים אלו להזמין א.נשים להרשם לשרת תוך עקיפת מנגנון הפיקוח על ההרשמה invite_users: הזמנת משתמשים invite_users_description: מאפשר למשתמשים להזמין אנשים חדשים לשרת manage_announcements: ניהול הכרזות @@ -1337,6 +1339,7 @@ he: invited_by: 'ניתן להצטרף אל %{domain} הודות להזמנה מאת:' preamble: אלו נקבעים ונאכפים ע"י המנחים של %{domain}. preamble_invited: לפני ההמשך יש להתחשב בחוקי המקום כפי שקבעו מנהלי הדיון על %{domain}. + read_more: לקריאה נוספת title: כמה חוקים בסיסיים. title_invited: קיבלת הזמנה. security: אבטחה diff --git a/config/locales/hu.yml b/config/locales/hu.yml index 27532f9f68..8bce2ee529 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1279,6 +1279,7 @@ hu: progress: confirm: E-mail megerősítése details: Saját adatok + list: Regisztrációs folyamat review: A felülvizsgálatunk rules: Szabályok elfogadása providers: @@ -1294,6 +1295,7 @@ hu: invited_by: 'Csatlakozhatsz a %{domain} kiszolgálóhoz, köszönhetően a meghívónak, melyet tőle kaptál:' preamble: Ezeket a(z) %{domain} moderátorai adjak meg és tartatják be. preamble_invited: Mielőtt csatlakozol, kérlek vedd fontolóra a %{domain} moderátorai által állított szabályokat. + read_more: Bővebben title: Néhány alapszabály. title_invited: Meghívtak. security: Biztonság diff --git a/config/locales/is.yml b/config/locales/is.yml index b73792eea7..1ea18a39d4 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -778,6 +778,8 @@ is: administrator_description: Notendur með þessa heimild fara framhjá öllum öðrum heimildum delete_user_data: Eyða gögnum notanda delete_user_data_description: Leyfir notendum að eyða gögnum annarra notenda án tafar + invite_bypass_approval: Bjóða notendum án samþykktar + invite_bypass_approval_description: Leyfir fólki sem boðið er á netþjóninn af þessum notendum að sleppa við samþykki umsjónaraðila invite_users: Bjóða notendum invite_users_description: Leyfir notendum að bjóða nýju fólki inn á netþjóninn manage_announcements: Sýsla með tilkynningar diff --git a/config/locales/it.yml b/config/locales/it.yml index 8c890f0baf..e01bed8b97 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -778,6 +778,8 @@ it: administrator_description: Gli utenti con questo permesso saranno esentati da ogni permesso delete_user_data: Cancella dati utente delete_user_data_description: Consente agli utenti di eliminare subito i dati degli altri utenti + invite_bypass_approval: Invita utenti senza revisione + invite_bypass_approval_description: Permette alle persone invitate al server da questi utenti, di aggirare l'approvazione della moderazione invite_users: Invita Utenti invite_users_description: Consente agli utenti di invitare nuove persone su questo server manage_announcements: Gestisci Annunci diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 40034e1cfe..b807451ece 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -587,6 +587,7 @@ lt: manage_taxonomies_description: Leidžia naudotojams peržiūrėti tendencingą turinį ir atnaujinti grotažymių nustatymus settings: branding: + preamble: Jūsų serverio pavadinimas išskiria jį iš kitų tinklo serverių. Ši informacija gali būti rodoma įvairiose aplinkose, pavyzdžiui, „Mastodon“ žiniatinklio sąsajoje, programėlėse, nuorodų peržiūrose kitose svetainėse, žinučių programėlėse ir pan. Dėl to geriausia, kad ši informacija būtų aiški, trumpa ir glausta. title: Firminio ženklo kūrimas captcha_enabled: desc_html: Tai priklauso nuo hCaptcha išorinių skriptų, kurie gali kelti susirūpinimą dėl saugumo ir privatumo. Be to, dėl to registracijos procesas kai kuriems žmonėms (ypač neįgaliesiems) gali būti gerokai sunkiau prieinami. Dėl šių priežasčių apsvarstyk alternatyvias priemones, pavyzdžiui, patvirtinimu arba kvietimu grindžiamą registraciją. diff --git a/config/locales/nn.yml b/config/locales/nn.yml index a105464962..f59ba33334 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -1279,6 +1279,7 @@ nn: progress: confirm: Stadfest e-post details: Opplysingane dine + list: Innmeldingsprosess review: Vår gjennomgang rules: Godta reglane providers: @@ -1294,6 +1295,7 @@ nn: invited_by: 'Du kan bli med i %{domain} takka vere invitasjonen du har fått frå:' preamble: Disse angis og håndheves av %{domain}-moderatorene. preamble_invited: Før du held fram, ver snill og sjå gjennom reglane bestemt av moderatorane av %{domain}. + read_more: Les meir title: Nokre grunnreglar. title_invited: Du har blitt invitert. security: Tryggleik diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml index fc6cbcaf51..e4ee1062ba 100644 --- a/config/locales/simple_form.be.yml +++ b/config/locales/simple_form.be.yml @@ -40,14 +40,14 @@ be: text: Вы можаце абскардзіць рашэнне толькі адзін раз defaults: autofollow: Людзі, якія зарэгістраваліся праз запрашэнне, аўтаматычна падпішуцца на вас - avatar: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions}} пікселяў + avatar: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions} пікселяў bot: Паведаміць іншым, што гэты ўліковы запіс у асноўным выконвае аўтаматычныя дзеянні і можа не кантралявацца context: Адзін ці некалькі кантэкстаў, да якіх трэба прымяніць фільтр current_password: У мэтах бяспекі, увядзіце пароль бягучага ўліковага запісу current_username: Каб пацвердзіць, увядзіце, калі ласка імя карыстальніка бягучага ўліковага запісу digest: Будзе даслана толькі пасля доўгага перыяду неактыўнасці і толькі, калі Вы атрымалі асабістыя паведамленні падчас Вашай адсутнасці email: Пацвярджэнне будзе выслана па электроннай пошце - header: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions}} пікселяў + header: WEBP, PNG, GIF ці JPG. Не больш за %{size}. Будзе сціснуты да памеру %{dimensions} пікселяў inbox_url: Капіраваць URL са старонкі рэтранслятара, якім вы хочаце карыстацца irreversible: Адфільтраваныя пасты прападуць незваротна, нават калі фільтр потым будзе выдалены locale: Мова карыстальніцкага інтэрфейсу, электронных паведамленняў і апавяшчэнняў diff --git a/config/locales/sq.yml b/config/locales/sq.yml index e622ce470b..caf915df78 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -773,6 +773,8 @@ sq: administrator_description: Përdoruesit me këtë leje do të anashkalojnë çdo leje delete_user_data: Të Fshijë të Dhëna Përdoruesi delete_user_data_description: U lejon përdoruesve të fshijnë pa humbur kohë të dhëna përdoruesish të tjerë + invite_bypass_approval: Ftoni Përdorues pa shqyrtim + invite_bypass_approval_description: U lejon personave të ftuar te shërbyesi nga këta përdorues të anashkalojnë miratim nga moderimi invite_users: Të Ftojë Përdorues invite_users_description: U lejon përdoruesve të ftojë te shërbyesi persona të rinj manage_announcements: Të Administrojë Njoftime diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 897ee840d4..d1b61289dc 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -775,6 +775,8 @@ sv: administrator_description: Användare med denna behörighet kommer att kringgå alla behörigheter delete_user_data: Ta bort användardata delete_user_data_description: Tillåter användare att omedelbart radera andra användares data + invite_bypass_approval: Bjud in användare utan granskning + invite_bypass_approval_description: Tillåter personer som är inbjudna till servern av dessa användare att kringgå moderationsgodkännande invite_users: Bjud in användare invite_users_description: Tillåter användare att bjuda in nya personer till servern manage_announcements: Hantera kungörelser diff --git a/config/locales/tr.yml b/config/locales/tr.yml index f7971c84c5..cc3c5a730b 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1295,6 +1295,7 @@ tr: invited_by: 'Aşağıdakinden aldığınız davet sayesinde %{domain} sunucusuna katılabilirsiniz:' preamble: Bunlar, %{domain} moderatörleri tarafından ayarlanmış ve uygulanmıştır. preamble_invited: Devam etmeden önce, %{domain} moderatörleri tarafından belirlenmiş temel kuralları gözden geçirin. + read_more: Devamını okuyun title: Bazı temel kurallar. title_invited: Davet edildiniz. security: Güvenlik diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 4da034dd77..f773dda729 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -762,6 +762,8 @@ vi: administrator_description: Người này có thể truy cập mọi quyền hạn delete_user_data: Xóa dữ liệu delete_user_data_description: Cho phép xóa dữ liệu của mọi người khác lập tức + invite_bypass_approval: Mời người dùng mà không xem lại + invite_bypass_approval_description: Cho phép những người được người dùng này mời vào máy chủ bỏ qua bước phê duyệt invite_users: Mời tham gia invite_users_description: Cho phép mời những người mới vào máy chủ manage_announcements: Quản lý thông báo diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index d845de83db..4585b729bc 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -762,6 +762,8 @@ zh-TW: administrator_description: 擁有此權限的使用者將會略過所有權限 delete_user_data: 刪除使用者資料 delete_user_data_description: 允許使用者立刻刪除其他使用者的資料 + invite_bypass_approval: 邀請未經審核的使用者 + invite_bypass_approval_description: 允許被這些使用者邀請至此伺服器的人們跳過管理審核許可 invite_users: 邀請使用者 invite_users_description: 允許使用者邀請新人加入伺服器 manage_announcements: 管理公告 diff --git a/spec/lib/activitypub/activity/delete_spec.rb b/spec/lib/activitypub/activity/delete_spec.rb index 48d2946b94..c2b3a63431 100644 --- a/spec/lib/activitypub/activity/delete_spec.rb +++ b/spec/lib/activitypub/activity/delete_spec.rb @@ -119,5 +119,28 @@ RSpec.describe ActivityPub::Activity::Delete do .to change { quote.reload.state }.to('revoked') end end + + context 'with a FeatureAuthorization', feature: :collections_federation do + let(:recipient) { Fabricate(:account) } + let(:approval_uri) { 'https://example.com/authorizations/1' } + let(:collection) { Fabricate(:collection, account: recipient) } + let!(:collection_item) { Fabricate(:collection_item, collection:, account: sender, state: :accepted, approval_uri:) } + let(:json) do + { + 'id' => 'https://example.com/accepts/1', + 'type' => 'Delete', + 'actor' => sender.uri, + 'to' => ActivityPub::TagManager.instance.uri_for(recipient), + 'object' => approval_uri, + } + end + + it 'revokes the collection item and federates a `Delete` activity' do + subject.perform + + expect(collection_item.reload).to be_revoked + expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job + end + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e4ef25bc76..35f0b98761 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -405,6 +405,31 @@ RSpec.describe User do end end + describe '#revoke_access!' do + subject(:user) { Fabricate(:user, disabled: false, current_sign_in_at: current_sign_in_at, last_sign_in_at: nil) } + + let(:current_sign_in_at) { Time.zone.now } + + let!(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id) } + + let(:redis_pipeline_stub) { instance_double(Redis::PipelinedConnection, publish: nil) } + + before do + allow(redis) + .to receive(:pipelined) + .and_yield(redis_pipeline_stub) + end + + it 'revokes tokens' do + user.revoke_access! + + expect(redis_pipeline_stub) + .to have_received(:publish).with("timeline:access_token:#{token.id}", { event: :kill }.to_json).once + + expect(token.reload.revoked?).to be true + end + end + describe '#enable!' do subject(:user) { Fabricate(:user, disabled: true) } diff --git a/spec/models/web/push_subscription_spec.rb b/spec/models/web/push_subscription_spec.rb index 3c2cd3bac1..b1e93687b3 100644 --- a/spec/models/web/push_subscription_spec.rb +++ b/spec/models/web/push_subscription_spec.rb @@ -93,4 +93,8 @@ RSpec.describe Web::PushSubscription do end end end + + describe 'Delegations' do + it { is_expected.to delegate_method(:token).to(:access_token).with_prefix(:associated_access) } + end end diff --git a/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb b/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb new file mode 100644 index 0000000000..37a78dbf41 --- /dev/null +++ b/spec/services/activitypub/fetch_featured_collections_collection_service_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FetchFeaturedCollectionsCollectionService do + subject { described_class.new } + + let(:account) { Fabricate(:remote_account, collections_url: 'https://example.com/account/featured_collections') } + let(:featured_collection_one) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/featured_collections/1', + 'type' => 'FeaturedCollection', + 'name' => 'Incredible people', + 'summary' => 'These are really amazing', + 'attributedTo' => account.uri, + 'sensitive' => false, + 'discoverable' => true, + 'totalItems' => 0, + } + end + let(:featured_collection_two) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/featured_collections/2', + 'type' => 'FeaturedCollection', + 'name' => 'Even cooler people', + 'summary' => 'These are just as amazing', + 'attributedTo' => account.uri, + 'sensitive' => false, + 'discoverable' => true, + 'totalItems' => 0, + } + end + let(:items) { [featured_collection_one, featured_collection_two] } + let(:collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Collection', + 'id' => account.collections_url, + 'items' => items, + } + end + + describe '#call' do + subject { described_class.new.call(account) } + + before do + stub_request(:get, account.collections_url) + .to_return_json(status: 200, body: collection_json, headers: { 'Content-Type': 'application/activity+json' }) + end + + shared_examples 'collection creation' do + it 'creates the expected collections' do + expect { subject }.to change(account.collections, :count).by(2) + expect(account.collections.pluck(:name)).to contain_exactly('Incredible people', 'Even cooler people') + end + end + + context 'when the endpoint is not paginated' do + context 'when all items are inlined' do + it_behaves_like 'collection creation' + end + + context 'when items are URIs' do + let(:items) { [featured_collection_one['id'], featured_collection_two['id']] } + + before do + [featured_collection_one, featured_collection_two].each do |featured_collection| + stub_request(:get, featured_collection['id']) + .to_return_json(status: 200, body: featured_collection, headers: { 'Content-Type': 'application/activity+json' }) + end + end + + it_behaves_like 'collection creation' + end + end + + context 'when the endpoint is a paginated Collection' do + let(:first_page) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'partOf' => account.collections_url, + 'id' => 'https://example.com/featured_collections/1/1', + 'items' => [featured_collection_one], + 'next' => second_page['id'], + } + end + let(:second_page) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'CollectionPage', + 'partOf' => account.collections_url, + 'id' => 'https://example.com/featured_collections/1/2', + 'items' => [featured_collection_two], + } + end + let(:collection_json) do + { + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Collection', + 'id' => account.collections_url, + 'first' => first_page['id'], + } + end + + before do + [first_page, second_page].each do |page| + stub_request(:get, page['id']) + .to_return_json(status: 200, body: page, headers: { 'Content-Type': 'application/activity+json' }) + end + end + + it_behaves_like 'collection creation' + end + end +end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index 1d8d8f8ac5..832f4e9729 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -63,7 +63,7 @@ RSpec.describe ActivityPub::ProcessAccountService do end end - context 'with collection URIs' do + context 'with collection URIs', feature: :collections_federation do let(:payload) do { 'id' => 'https://foo.test', @@ -81,13 +81,16 @@ RSpec.describe ActivityPub::ProcessAccountService do .to_return(status: 200, body: '', headers: {}) end - it 'parses and sets the URIs' do + it 'parses and sets the URIs, queues jobs to synchronize' do account = subject.call('alice', 'example.com', payload) expect(account.featured_collection_url).to eq 'https://foo.test/featured' expect(account.followers_url).to eq 'https://foo.test/followers' expect(account.following_url).to eq 'https://foo.test/following' expect(account.collections_url).to eq 'https://foo.test/featured_collections' + + expect(ActivityPub::SynchronizeFeaturedCollectionWorker).to have_enqueued_sidekiq_job + expect(ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker).to have_enqueued_sidekiq_job end end diff --git a/spec/services/activitypub/verify_featured_item_service_spec.rb b/spec/services/activitypub/verify_featured_item_service_spec.rb index 5698c23155..f0f1661b6d 100644 --- a/spec/services/activitypub/verify_featured_item_service_spec.rb +++ b/spec/services/activitypub/verify_featured_item_service_spec.rb @@ -55,7 +55,7 @@ RSpec.describe ActivityPub::VerifyFeaturedItemService do let(:stubbed_service) { instance_double(ActivityPub::FetchRemoteAccountService) } before do - allow(stubbed_service).to receive(:call).with('https://example.com/actor/1') { featured_account } + allow(stubbed_service).to receive(:call).with('https://example.com/actor/1', request_id: nil) { featured_account } allow(ActivityPub::FetchRemoteAccountService).to receive(:new).and_return(stubbed_service) end diff --git a/spec/services/delete_collection_item_service_spec.rb b/spec/services/delete_collection_item_service_spec.rb index 099671e8fc..bdd37ad4a8 100644 --- a/spec/services/delete_collection_item_service_spec.rb +++ b/spec/services/delete_collection_item_service_spec.rb @@ -18,5 +18,13 @@ RSpec.describe DeleteCollectionItemService do expect(ActivityPub::AccountRawDistributionWorker).to have_enqueued_sidekiq_job end + + context 'when `revoke` is set to true' do + it 'revokes the collection item' do + subject.call(collection_item, revoke: true) + + expect(collection_item.reload).to be_revoked + end + end end end diff --git a/spec/system/auth/passwords_spec.rb b/spec/system/auth/passwords_spec.rb index 83853d68fa..55f9c68938 100644 --- a/spec/system/auth/passwords_spec.rb +++ b/spec/system/auth/passwords_spec.rb @@ -11,7 +11,14 @@ RSpec.describe 'Auth Passwords' do describe 'Resetting a password', :inline_jobs do let(:new_password) { 'New.Pass.123' } - before { allow(Devise).to receive(:pam_authentication).and_return(false) } # Avoid the "seamless external" path + before do + allow(Devise).to receive(:pam_authentication).and_return(false) # Avoid the "seamless external" path + + # Disable wrapstodon to avoid redis calls that we don't want to stub + Setting.wrapstodon = false + + allow(redis).to receive(:publish) + end it 'initiates reset, sends link, resets password from form, clears data' do visit new_user_password_path @@ -31,6 +38,10 @@ RSpec.describe 'Auth Passwords' do .to be_present .and be_valid_password(new_password) + # Disables the token associated with the session + expect(redis) + .to have_received(:publish).with("timeline:access_token:#{session_activation.access_token.id}", { event: :kill }.to_json).once + # Deactivate session expect(user_session_count) .to eq(0) diff --git a/spec/system/streaming/streaming_spec.rb b/spec/system/streaming/streaming_spec.rb index f5d3ba1142..53aa6f21f7 100644 --- a/spec/system/streaming/streaming_spec.rb +++ b/spec/system/streaming/streaming_spec.rb @@ -75,6 +75,23 @@ RSpec.describe 'Streaming', :inline_jobs, :streaming do end end + context 'when destroying a session activation tied to the used token' do + let(:session_activation) { Fabricate(:session_activation, user: user) } + let(:token) { session_activation.access_token } + + it 'disconnects the client' do + streaming_client.connect + + expect(streaming_client.status).to eq(101) + expect(streaming_client.open?).to be(true) + + session_activation.destroy! + + expect(streaming_client.wait_for(:closed).code).to be(1000) + expect(streaming_client.open?).to be(false) + end + end + context 'with a disabled user account' do before do user.disable! diff --git a/spec/workers/activitypub/process_featured_item_worker_spec.rb b/spec/workers/activitypub/process_featured_item_worker_spec.rb index f27ec21c35..02a9edfc47 100644 --- a/spec/workers/activitypub/process_featured_item_worker_spec.rb +++ b/spec/workers/activitypub/process_featured_item_worker_spec.rb @@ -19,7 +19,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemWorker do it 'calls the service to process the item' do subject.perform(collection.id, object) - expect(stubbed_service).to have_received(:call).with(collection, object) + expect(stubbed_service).to have_received(:call).with(collection, object, request_id: nil) end end end diff --git a/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb b/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb new file mode 100644 index 0000000000..6fcb9d02b2 --- /dev/null +++ b/spec/workers/activitypub/synchronize_featured_collections_collection_worker_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::SynchronizeFeaturedCollectionsCollectionWorker do + let(:worker) { described_class.new } + let(:service) { instance_double(ActivityPub::FetchFeaturedCollectionsCollectionService, call: true) } + + describe '#perform' do + before do + allow(ActivityPub::FetchFeaturedCollectionsCollectionService).to receive(:new).and_return(service) + end + + let(:account) { Fabricate(:account) } + + it 'sends the account to the service' do + worker.perform(account.id) + + expect(service).to have_received(:call).with(account, request_id: nil) + end + + it 'returns true for non-existent record' do + result = worker.perform(123_123_123) + + expect(result).to be(true) + end + end +end diff --git a/spec/workers/activitypub/verify_featured_item_worker_spec.rb b/spec/workers/activitypub/verify_featured_item_worker_spec.rb index bbea044c34..d7d31b3510 100644 --- a/spec/workers/activitypub/verify_featured_item_worker_spec.rb +++ b/spec/workers/activitypub/verify_featured_item_worker_spec.rb @@ -14,7 +14,7 @@ RSpec.describe ActivityPub::VerifyFeaturedItemWorker do it 'sends the status to the service' do worker.perform(collection_item.id, 'https://example.com/authorizations/1') - expect(service).to have_received(:call).with(collection_item, 'https://example.com/authorizations/1') + expect(service).to have_received(:call).with(collection_item, 'https://example.com/authorizations/1', request_id: nil) end it 'returns nil for non-existent record' do diff --git a/yarn.lock b/yarn.lock index 536a2666b8..4094a8629e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6303,9 +6303,9 @@ __metadata: linkType: hard "core-js@npm:^3.30.2, core-js@npm:^3.45.0": - version: 3.48.0 - resolution: "core-js@npm:3.48.0" - checksum: 10c0/6c3115900dd7cce9fab74c07cb262b3517fc250d02e8fd2ff34f80bda5f43a28482a909dbc4491dc6e1ddd9807f57a7df4c3eeecd1f202b7d9c8bfe25f9d680c + version: 3.49.0 + resolution: "core-js@npm:3.49.0" + checksum: 10c0/2e42edb47eda38fd5368380131623c8aa5d4a6b42164125b17744bdc08fa5ebbbdd06b4b4aa6ca3663470a560b0f2fba48e18f142dfe264b0039df85bc625694 languageName: node linkType: hard @@ -8683,8 +8683,8 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.10.0 - resolution: "ioredis@npm:5.10.0" + version: 5.10.1 + resolution: "ioredis@npm:5.10.1" dependencies: "@ioredis/commands": "npm:1.5.1" cluster-key-slot: "npm:^1.1.0" @@ -8695,7 +8695,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/294e8cdef963f922b04ad023d4165f1d399b27279b9890f3e1311da57fb2a082c8dca52d18bd14a54acd6fb82853783641a0c47ff18661a8756221049f807e88 + checksum: 10c0/d0507b52520d3bdd5dacaa33aed9dd3133794d8633b43a6b7fc3199a5e73f92cb77409f6904abe68e3221a95a630d97073b8c1c9e2c0c7613124db67e97c0eb0 languageName: node linkType: hard