diff --git a/.env.production.sample b/.env.production.sample index 083abdd687..e341c07801 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -318,21 +318,3 @@ MAX_POLL_OPTION_CHARS=100 # ----------------------- IP_RETENTION_PERIOD=31556952 SESSION_RETENTION_PERIOD=31556952 - -# Fetch All Replies Behavior -# -------------------------- - -# Period to wait between fetching replies (in minutes) -FETCH_REPLIES_COOLDOWN_MINUTES=15 - -# Period to wait after a post is first created before fetching its replies (in minutes) -FETCH_REPLIES_INITIAL_WAIT_MINUTES=5 - -# Max number of replies to fetch - total, recursively through a whole reply tree -FETCH_REPLIES_MAX_GLOBAL=1000 - -# Max number of replies to fetch - for a single post -FETCH_REPLIES_MAX_SINGLE=500 - -# Max number of replies Collection pages to fetch - total -FETCH_REPLIES_MAX_PAGES=500 diff --git a/.gitignore b/.gitignore index db63bc07f0..4727d9ec27 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ /public/packs /public/packs-dev /public/packs-test +stats.html .env .env.production node_modules/ diff --git a/Gemfile.lock b/Gemfile.lock index 8d0a69e05e..2a0da53d4b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM public_suffix (>= 2.0.2, < 7.0) aes_key_wrap (1.1.0) android_key_attestation (0.3.0) - annotaterb (4.19.0) + annotaterb (4.20.0) activerecord (>= 6.0.0) activesupport (>= 6.0.0) ast (2.4.3) @@ -116,7 +116,7 @@ GEM base64 (0.3.0) bcp47_spec (0.2.1) bcrypt (3.1.20) - benchmark (0.4.1) + benchmark (0.5.0) better_errors (2.10.1) erubi (>= 1.0.0) rack (>= 0.9.0) @@ -168,7 +168,7 @@ GEM cose (1.3.1) cbor (~> 0.5.9) openssl-signature_algorithm (~> 1.0) - crack (1.0.0) + crack (1.0.1) bigdecimal rexml crass (1.0.6) @@ -190,10 +190,10 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-two-factor (6.1.0) - activesupport (>= 7.0, < 8.1) + devise-two-factor (6.2.0) + activesupport (>= 7.0, < 8.2) devise (~> 4.0) - railties (>= 7.0, < 8.1) + railties (>= 7.0, < 8.2) rotp (~> 6.0) devise_pam_authenticatable2 (9.2.0) devise (>= 4.0.0) @@ -224,7 +224,7 @@ GEM mail (~> 2.7) email_validator (2.2.4) activemodel - erb (5.0.2) + erb (5.1.1) erubi (1.13.1) et-orbi (1.4.0) tzinfo @@ -426,7 +426,8 @@ GEM loofah (2.24.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) - mail (2.8.1) + mail (2.9.0) + logger mini_mime (>= 0.1.1) net-imap net-pop @@ -442,7 +443,7 @@ GEM mime-types-data (3.2025.0924) mini_mime (1.1.5) mini_portile2 (2.8.9) - minitest (5.25.5) + minitest (5.26.0) msgpack (1.8.0) multi_json (1.17.0) mutex_m (0.3.0) @@ -705,9 +706,9 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - responders (3.1.1) - actionpack (>= 5.2) - railties (>= 5.2) + responders (3.2.0) + actionpack (>= 7.0) + railties (>= 7.0) rexml (3.4.4) rotp (6.3.0) rouge (4.6.1) @@ -821,9 +822,9 @@ GEM thor (>= 1.0, < 3.0) simple-navigation (4.4.0) activesupport (>= 2.3.2) - simple_form (5.3.1) - actionpack (>= 5.2) - activemodel (>= 5.2) + simple_form (5.4.0) + actionpack (>= 7.0) + activemodel (>= 7.0) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) @@ -910,7 +911,7 @@ GEM activesupport faraday (~> 2.0) faraday-follow_redirects - webmock (3.25.1) + webmock (3.26.0) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 6619899041..84ba142b70 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -159,7 +159,7 @@ class Api::V1::StatusesController < Api::BaseController end def set_quoted_status - @quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present? + @quoted_status = Status.find(status_params[:quoted_status_id])&.proper if status_params[:quoted_status_id].present? authorize(@quoted_status, :quote?) if @quoted_status.present? rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError # TODO: distinguish between non-existing and non-quotable posts diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index fea3eb0d79..dd1956446d 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -70,7 +70,7 @@ function loaded() { }; document.querySelectorAll('.emojify').forEach((content) => { - content.innerHTML = emojify(content.innerHTML, {}, true); // Force emojify as public doesn't load the new emoji system. + content.innerHTML = emojify(content.innerHTML); }); document diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index f321418925..b85c0dc509 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -624,6 +624,7 @@ export function fetchComposeSuggestions(token) { fetchComposeSuggestionsEmojis(dispatch, getState, token); break; case '#': + case '#': fetchComposeSuggestionsTags(dispatch, getState, token); break; default: @@ -665,11 +666,11 @@ export function selectComposeSuggestion(position, token, suggestion, path) { dispatch(useEmoji(suggestion)); } else if (suggestion.type === 'hashtag') { - completion = `#${suggestion.name}`; - startPosition = position - 1; + completion = suggestion.name.slice(token.length - 1); + startPosition = position + token.length; } else if (suggestion.type === 'account') { - completion = getState().getIn(['accounts', suggestion.id, 'acct']); - startPosition = position; + completion = `@${getState().getIn(['accounts', suggestion.id, 'acct'])}`; + startPosition = position - 1; } // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that @@ -729,7 +730,7 @@ function insertIntoTagHistory(recognizedTags, text) { // complicated because of new normalization rules, it's no longer just // a case sensitivity issue const names = recognizedTags.map(tag => { - const matches = text.match(new RegExp(`#${tag.name}`, 'i')); + const matches = text.match(new RegExp(`[##]${tag.name}`, 'i')); if (matches && matches.length > 0) { return matches[0].slice(1); diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index 7723379804..32c3d76666 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -1,8 +1,5 @@ import escapeTextContentForBrowser from 'escape-html'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; - -import emojify from '../../features/emoji/emoji'; import { expandSpoilers } from '../../initial_state'; const domParser = new DOMParser(); @@ -88,11 +85,10 @@ export function normalizeStatus(status, normalOldStatus) { const spoilerText = normalStatus.spoiler_text || ''; const searchContent = ([spoilerText, status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); - const emojiMap = makeEmojiMap(normalStatus.emojis); normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; - normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); - normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); + normalStatus.contentHtml = normalStatus.content; + normalStatus.spoilerHtml = escapeTextContentForBrowser(spoilerText); normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; // Remove quote fallback link from the DOM so it doesn't mess with paragraph margins @@ -128,14 +124,12 @@ export function normalizeStatus(status, normalOldStatus) { } export function normalizeStatusTranslation(translation, status) { - const emojiMap = makeEmojiMap(status.get('emojis').toJS()); - const normalTranslation = { detected_source_language: translation.detected_source_language, language: translation.language, provider: translation.provider, - contentHtml: emojify(translation.content, emojiMap), - spoilerHtml: emojify(escapeTextContentForBrowser(translation.spoiler_text), emojiMap), + contentHtml: translation.content, + spoilerHtml: escapeTextContentForBrowser(translation.spoiler_text), spoiler_text: translation.spoiler_text, }; @@ -149,9 +143,8 @@ export function normalizeStatusTranslation(translation, status) { export function normalizeAnnouncement(announcement) { const normalAnnouncement = { ...announcement }; - const emojiMap = makeEmojiMap(normalAnnouncement.emojis); - normalAnnouncement.contentHtml = emojify(normalAnnouncement.content, emojiMap); + normalAnnouncement.contentHtml = normalAnnouncement.content; return normalAnnouncement; } diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 478e0cae45..4299bad5c3 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -32,13 +32,20 @@ import { const randomUpTo = max => Math.floor(Math.random() * Math.floor(max)); +/** + * @typedef {import('mastodon/store').AppDispatch} Dispatch + * @typedef {import('mastodon/store').GetState} GetState + * @typedef {import('redux').UnknownAction} UnknownAction + * @typedef {function(Dispatch, GetState): Promise} FallbackFunction + */ + /** * @param {string} timelineId * @param {string} channelName * @param {Object.} params * @param {Object} options - * @param {function(Function, Function): Promise} [options.fallback] - * @param {function(): void} [options.fillGaps] + * @param {FallbackFunction} [options.fallback] + * @param {function(): UnknownAction} [options.fillGaps] * @param {function(object): boolean} [options.accept] * @returns {function(): void} */ @@ -46,13 +53,14 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti const { messages } = getLocale(); return connectStream(channelName, params, (dispatch, getState) => { + // @ts-ignore const locale = getState().getIn(['meta', 'locale']); // @ts-expect-error let pollingId; /** - * @param {function(Function, Function): Promise} fallback + * @param {FallbackFunction} fallback */ const useFallback = async fallback => { @@ -132,7 +140,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti }; /** - * @param {Function} dispatch + * @param {Dispatch} dispatch */ async function refreshHomeTimelineAndNotification(dispatch) { await dispatch(expandHomeTimeline({ maxId: undefined })); @@ -151,7 +159,11 @@ async function refreshHomeTimelineAndNotification(dispatch) { * @returns {function(): void} */ export const connectUserStream = () => - connectTimelineStream('home', 'user', {}, { fallback: refreshHomeTimelineAndNotification, fillGaps: fillHomeTimelineGaps }); + connectTimelineStream('home', 'user', {}, { + fallback: refreshHomeTimelineAndNotification, + // @ts-expect-error + fillGaps: fillHomeTimelineGaps + }); /** * @param {Object} options @@ -159,7 +171,10 @@ export const connectUserStream = () => * @returns {function(): void} */ export const connectCommunityStream = ({ onlyMedia } = {}) => - connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) }); + connectTimelineStream(`community${onlyMedia ? ':media' : ''}`, `public:local${onlyMedia ? ':media' : ''}`, {}, { + // @ts-expect-error + fillGaps: () => (fillCommunityTimelineGaps({ onlyMedia })) + }); /** * @param {Object} options @@ -168,7 +183,10 @@ export const connectCommunityStream = ({ onlyMedia } = {}) => * @returns {function(): void} */ export const connectPublicStream = ({ onlyMedia, onlyRemote } = {}) => - connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) }); + connectTimelineStream(`public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`, {}, { + // @ts-expect-error + fillGaps: () => fillPublicTimelineGaps({ onlyMedia, onlyRemote }) + }); /** * @param {string} columnId @@ -191,4 +209,7 @@ export const connectDirectStream = () => * @returns {function(): void} */ export const connectListStream = listId => - connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { fillGaps: () => fillListTimelineGaps(listId) }); + connectTimelineStream(`list:${listId}`, 'list', { list: listId }, { + // @ts-expect-error + fillGaps: () => fillListTimelineGaps(listId) + }); diff --git a/app/javascript/mastodon/components/account_bio.tsx b/app/javascript/mastodon/components/account_bio.tsx index e87ae654fd..6d4ab1ddd4 100644 --- a/app/javascript/mastodon/components/account_bio.tsx +++ b/app/javascript/mastodon/components/account_bio.tsx @@ -1,11 +1,6 @@ -import { useCallback } from 'react'; - import classNames from 'classnames'; -import { useLinks } from 'mastodon/hooks/useLinks'; - import { useAppSelector } from '../store'; -import { isModernEmojiEnabled } from '../utils/environment'; import { EmojiHTML } from './emoji/html'; import { useElementHandledLink } from './status/handled_link'; @@ -21,22 +16,6 @@ export const AccountBio: React.FC = ({ accountId, showDropdown = false, }) => { - const handleClick = useLinks(showDropdown); - const handleNodeChange = useCallback( - (node: HTMLDivElement | null) => { - if ( - !showDropdown || - !node || - node.childNodes.length === 0 || - isModernEmojiEnabled() - ) { - return; - } - addDropdownToHashtags(node, accountId); - }, - [showDropdown, accountId], - ); - const htmlHandlers = useElementHandledLink({ hashtagAccountId: showDropdown ? accountId : undefined, }); @@ -62,30 +41,7 @@ export const AccountBio: React.FC = ({ htmlString={note} extraEmojis={extraEmojis} className={classNames(className, 'translate')} - onClickCapture={handleClick} - ref={handleNodeChange} {...htmlHandlers} /> ); }; - -function addDropdownToHashtags(node: HTMLElement | null, accountId: string) { - if (!node) { - return; - } - for (const childNode of node.childNodes) { - if (!(childNode instanceof HTMLElement)) { - continue; - } - if ( - childNode instanceof HTMLAnchorElement && - (childNode.classList.contains('hashtag') || - childNode.innerText.startsWith('#')) && - !childNode.dataset.menuHashtag - ) { - childNode.dataset.menuHashtag = accountId; - } else if (childNode.childNodes.length > 0) { - addDropdownToHashtags(childNode, accountId); - } - } -} diff --git a/app/javascript/mastodon/components/autosuggest_input.jsx b/app/javascript/mastodon/components/autosuggest_input.jsx index f707a18e1d..267c044215 100644 --- a/app/javascript/mastodon/components/autosuggest_input.jsx +++ b/app/javascript/mastodon/components/autosuggest_input.jsx @@ -61,7 +61,7 @@ export default class AutosuggestInput extends ImmutablePureComponent { static defaultProps = { autoFocus: true, - searchTokens: ['@', ':', '#'], + searchTokens: ['@', '@', ':', '#', '#'], }; state = { diff --git a/app/javascript/mastodon/components/autosuggest_textarea.jsx b/app/javascript/mastodon/components/autosuggest_textarea.jsx index 68cf9e17fc..137bad9b7e 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.jsx +++ b/app/javascript/mastodon/components/autosuggest_textarea.jsx @@ -25,7 +25,7 @@ const textAtCursorMatchesToken = (str, caretPosition) => { word = str.slice(left, right + caretPosition); } - if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) { + if (!word || word.trim().length < 3 || ['@', '@', ':', '#', '#'].indexOf(word[0]) === -1) { return [null, null]; } diff --git a/app/javascript/mastodon/components/display_name/display_name.stories.tsx b/app/javascript/mastodon/components/display_name/display_name.stories.tsx index d546fdd135..6f1819a557 100644 --- a/app/javascript/mastodon/components/display_name/display_name.stories.tsx +++ b/app/javascript/mastodon/components/display_name/display_name.stories.tsx @@ -74,6 +74,6 @@ export const Linked: Story = { acct: username, }) : undefined; - return ; + return ; }, }; diff --git a/app/javascript/mastodon/components/display_name/no-domain.tsx b/app/javascript/mastodon/components/display_name/no-domain.tsx index ee6e84050c..530e0a08e0 100644 --- a/app/javascript/mastodon/components/display_name/no-domain.tsx +++ b/app/javascript/mastodon/components/display_name/no-domain.tsx @@ -9,9 +9,8 @@ import { Skeleton } from '../skeleton'; import type { DisplayNameProps } from './index'; export const DisplayNameWithoutDomain: FC< - Omit & - ComponentPropsWithoutRef<'span'> -> = ({ account, className, children, ...props }) => { + Omit & ComponentPropsWithoutRef<'span'> +> = ({ account, className, children, localDomain: _, ...props }) => { return ( & - ComponentPropsWithoutRef<'span'> -> = ({ account, ...props }) => { + Omit & ComponentPropsWithoutRef<'span'> +> = ({ account, localDomain: _, ...props }) => { if (!account) { return null; } diff --git a/app/javascript/mastodon/components/emoji/context.tsx b/app/javascript/mastodon/components/emoji/context.tsx index 730ae743ed..3682b94141 100644 --- a/app/javascript/mastodon/components/emoji/context.tsx +++ b/app/javascript/mastodon/components/emoji/context.tsx @@ -7,8 +7,6 @@ import { useState, } from 'react'; -import classNames from 'classnames'; - import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize'; import { autoPlayGif } from '@/mastodon/initial_state'; import { polymorphicForwardRef } from '@/types/polymorphic'; @@ -65,11 +63,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef< const parentContext = useContext(AnimateEmojiContext); if (parentContext !== null) { return ( - + {children} ); @@ -78,7 +72,7 @@ export const AnimateEmojiProvider = polymorphicForwardRef< return ( ( +export const EmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( ( { extraEmojis, @@ -59,32 +56,4 @@ export const ModernEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( ); }, ); -ModernEmojiHTML.displayName = 'ModernEmojiHTML'; - -export const LegacyEmojiHTML = polymorphicForwardRef<'div', EmojiHTMLProps>( - (props, ref) => { - const { - as: asElement, - htmlString, - extraEmojis, - className, - onElement, - onAttribute, - ...rest - } = props; - const Wrapper = asElement ?? 'div'; - return ( - - ); - }, -); -LegacyEmojiHTML.displayName = 'LegacyEmojiHTML'; - -export const EmojiHTML = isModernEmojiEnabled() - ? ModernEmojiHTML - : LegacyEmojiHTML; +EmojiHTML.displayName = 'EmojiHTML'; diff --git a/app/javascript/mastodon/components/hover_card_account.tsx b/app/javascript/mastodon/components/hover_card_account.tsx index 471d488415..b51af40e94 100644 --- a/app/javascript/mastodon/components/hover_card_account.tsx +++ b/app/javascript/mastodon/components/hover_card_account.tsx @@ -23,8 +23,6 @@ import { domain } from 'mastodon/initial_state'; import { getAccountHidden } from 'mastodon/selectors/accounts'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; -import { useLinks } from '../hooks/useLinks'; - export const HoverCardAccount = forwardRef< HTMLDivElement, { accountId?: string } @@ -66,8 +64,6 @@ export const HoverCardAccount = forwardRef< !isMutual && !isFollower; - const handleClick = useLinks(); - return (

-
+
onParentElement?.(...args) ?? onLinkElement(...args), [onLinkElement, onParentElement], ); - return ; + return ; }, ); diff --git a/app/javascript/mastodon/components/poll.tsx b/app/javascript/mastodon/components/poll.tsx index a9229e6ee4..98954fc2d9 100644 --- a/app/javascript/mastodon/components/poll.tsx +++ b/app/javascript/mastodon/components/poll.tsx @@ -13,9 +13,7 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import { openModal } from 'mastodon/actions/modal'; import { fetchPoll, vote } from 'mastodon/actions/polls'; import { Icon } from 'mastodon/components/icon'; -import emojify from 'mastodon/features/emoji/emoji'; import { useIdentity } from 'mastodon/identity_context'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import type * as Model from 'mastodon/models/poll'; import type { Status } from 'mastodon/models/status'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; @@ -235,12 +233,11 @@ const PollOption: React.FC = (props) => { let titleHtml = option.translation?.titleHtml ?? option.titleHtml; if (!titleHtml) { - const emojiMap = makeEmojiMap(poll.emojis); - titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap); + titleHtml = escapeTextContentForBrowser(title); } return titleHtml; - }, [option, poll, title]); + }, [option, title]); // Handlers const handleOptionChange = useCallback(() => { diff --git a/app/javascript/mastodon/components/status/handled_link.tsx b/app/javascript/mastodon/components/status/handled_link.tsx index 59305d5734..be816e9853 100644 --- a/app/javascript/mastodon/components/status/handled_link.tsx +++ b/app/javascript/mastodon/components/status/handled_link.tsx @@ -26,7 +26,12 @@ export const HandledLink: FC> = ({ ...props }) => { // Handle hashtags - if (text.startsWith('#') || prevText?.endsWith('#')) { + if ( + text.startsWith('#') || + prevText?.endsWith('#') || + text.startsWith('#') || + prevText?.endsWith('#') + ) { const hashtag = text.slice(1).trim(); return ( ({ languages: state.getIn(['server', 'translationLanguages', 'items']), }); +const compareUrls = (href1, href2) => { + try { + const url1 = new URL(href1); + const url2 = new URL(href2); + + return url1.origin === url2.origin && url1.pathname === url2.pathname && url1.search === url2.search; + } catch { + return false; + } +}; + class StatusContent extends PureComponent { static propTypes = { identity: identityContextPropShape, @@ -108,41 +117,6 @@ class StatusContent extends PureComponent { onCollapsedToggle(collapsed); } - - // Exit if modern emoji is enabled, as it handles links using the HandledLink component. - if (isModernEmojiEnabled()) { - return; - } - - const links = node.querySelectorAll('a'); - - let link, mention; - - for (var i = 0; i < links.length; ++i) { - link = links[i]; - - if (link.classList.contains('status-link')) { - continue; - } - - link.classList.add('status-link'); - - mention = this.props.status.get('mentions').find(item => link.href === item.get('url')); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', `@${mention.get('acct')}`); - link.setAttribute('href', `/@${mention.get('acct')}`); - link.setAttribute('data-hover-card-account', mention.get('id')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); - link.setAttribute('data-menu-hashtag', this.props.status.getIn(['account', 'id'])); - } else { - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - } } componentDidMount () { @@ -153,22 +127,6 @@ class StatusContent extends PureComponent { this._updateStatusLinks(); } - onMentionClick = (mention, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${mention.get('acct')}`); - } - }; - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, ''); - - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/tags/${hashtag}`); - } - }; - handleMouseDown = (e) => { this.startXY = [e.clientX, e.clientY]; }; @@ -206,7 +164,7 @@ class StatusContent extends PureComponent { handleElement = (element, { key, ...props }, children) => { if (element instanceof HTMLAnchorElement) { - const mention = this.props.status.get('mentions').find(item => element.href === item.get('url')); + const mention = this.props.status.get('mentions').find(item => compareUrls(element.href, item.get('url'))); return ( { - if (isModernEmojiEnabled()) { - return html; - } - const document = domParser.parseFromString(html, 'text/html').documentElement; - - document.querySelectorAll('a[rel]').forEach((link) => { - link.rel = link.rel - .split(' ') - .filter((x: string) => x !== 'me') - .join(' '); - }); - - const body = document.querySelector('body'); - return body?.innerHTML ?? ''; -}; - const onAttribute: OnAttributeHandler = (name, value, tagName) => { if (name === 'rel' && tagName === 'a') { if (value === 'me') { @@ -47,10 +27,6 @@ interface Props { export const VerifiedBadge: React.FC = ({ link }) => ( - + ); diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 2bf636d060..040ca16c72 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -49,7 +49,6 @@ import { ShortNumber } from 'mastodon/components/short_number'; import { AccountNote } from 'mastodon/features/account/components/account_note'; import { DomainPill } from 'mastodon/features/account/components/domain_pill'; import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container'; -import { useLinks } from 'mastodon/hooks/useLinks'; import { useIdentity } from 'mastodon/identity_context'; import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; @@ -198,7 +197,6 @@ export const AccountHeader: React.FC<{ state.relationships.get(accountId), ); const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); - const handleLinkClick = useLinks(); const handleBlock = useCallback(() => { if (!account) { @@ -852,10 +850,7 @@ export const AccountHeader: React.FC<{ {!(suspended || hidden) && (
-
+
{account.id !== me && signedIn && ( )} diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index cc0d6fe195..8ca86d16cd 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,6 +1,5 @@ import Trie from 'substring-trie'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { assetHost } from 'mastodon/utils/config'; import { autoPlayGif } from '../../initial_state'; @@ -153,13 +152,9 @@ const emojifyNode = (node, customEmojis) => { * Legacy emoji processing function. * @param {string} str * @param {object} customEmojis - * @param {boolean} force If true, always emojify even if modern emoji is enabled * @returns {string} */ -const emojify = (str, customEmojis = {}, force = false) => { - if (isModernEmojiEnabled() && !force) { - return str; - } +const emojify = (str, customEmojis = {}) => { const wrapper = document.createElement('div'); wrapper.innerHTML = str; diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.mjs b/app/javascript/mastodon/features/emoji/emoji_compressed.mjs index db8a4bc122..c565b861fd 100644 --- a/app/javascript/mastodon/features/emoji/emoji_compressed.mjs +++ b/app/javascript/mastodon/features/emoji/emoji_compressed.mjs @@ -14,8 +14,7 @@ import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data'; import data from './emoji_data.json'; import emojiMap from './emoji_map.json'; -import { unicodeToFilename } from './unicode_to_filename'; -import { unicodeToUnifiedName } from './unicode_to_unified_name'; +import { unicodeToFilename, unicodeToUnifiedName } from './unicode_utils'; emojiMartUncompress(data); diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts index 1315518179..59d995e25f 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts +++ b/app/javascript/mastodon/features/emoji/emoji_mart_data_light.ts @@ -9,7 +9,7 @@ import type { ShortCodesToEmojiData, } from 'virtual:mastodon-emoji-compressed'; -import { unicodeToUnifiedName } from './unicode_to_unified_name'; +import { unicodeToUnifiedName } from './unicode_utils'; type Emojis = Record< NonNullable, @@ -23,7 +23,7 @@ type Emojis = Record< const [ shortCodesToEmojiData, - skins, + _skins, categories, short_names, _emojisWithoutShortCodes, @@ -47,4 +47,4 @@ Object.keys(shortCodesToEmojiData).forEach((shortCode) => { }; }); -export { emojis, skins, categories, short_names }; +export { emojis, categories, short_names }; diff --git a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js index 83e154b0b2..038dd120c9 100644 --- a/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js +++ b/app/javascript/mastodon/features/emoji/emoji_mart_search_light.js @@ -1,7 +1,7 @@ // This code is largely borrowed from: // https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js -import * as data from './emoji_mart_data_light'; +import { emojis, categories } from './emoji_mart_data_light'; import { getData, getSanitizedData, uniq, intersect } from './emoji_utils'; let originalPool = {}; @@ -10,8 +10,8 @@ let emojisList = {}; let emoticonsList = {}; let customEmojisList = []; -for (let emoji in data.emojis) { - let emojiData = data.emojis[emoji]; +for (let emoji in emojis) { + let emojiData = emojis[emoji]; let { short_names, emoticons } = emojiData; let id = short_names[0]; @@ -84,14 +84,14 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (include.length || exclude.length) { pool = {}; - data.categories.forEach(category => { + categories.forEach(category => { let isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true; let isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false; if (!isIncluded || isExcluded) { return; } - category.emojis.forEach(emojiId => pool[emojiId] = data.emojis[emojiId]); + category.emojis.forEach(emojiId => pool[emojiId] = emojis[emojiId]); }); if (custom.length) { @@ -171,7 +171,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo if (results) { if (emojisToShowFilter) { - results = results.filter((result) => emojisToShowFilter(data.emojis[result.id])); + results = results.filter((result) => emojisToShowFilter(emojis[result.id])); } if (results && results.length > maxResults) { diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.tsx b/app/javascript/mastodon/features/emoji/emoji_picker.tsx index 38363d4310..37fc94dde7 100644 --- a/app/javascript/mastodon/features/emoji/emoji_picker.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_picker.tsx @@ -2,7 +2,6 @@ import type { EmojiProps, PickerProps } from 'emoji-mart'; import EmojiRaw from 'emoji-mart/dist-es/components/emoji/nimble-emoji'; import PickerRaw from 'emoji-mart/dist-es/components/picker/nimble-picker'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { assetHost } from 'mastodon/utils/config'; import { EMOJI_MODE_NATIVE } from './constants'; @@ -27,7 +26,7 @@ const Emoji = ({ sheetSize={sheetSize} sheetColumns={sheetColumns} sheetRows={sheetRows} - native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()} + native={mode === EMOJI_MODE_NATIVE} backgroundImageFn={backgroundImageFn} {...props} /> @@ -51,7 +50,7 @@ const Picker = ({ sheetColumns={sheetColumns} sheetRows={sheetRows} backgroundImageFn={backgroundImageFn} - native={mode === EMOJI_MODE_NATIVE && isModernEmojiEnabled()} + native={mode === EMOJI_MODE_NATIVE} {...props} /> ); diff --git a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts index a53496be2a..ecf36e3ea8 100644 --- a/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts +++ b/app/javascript/mastodon/features/emoji/emoji_unicode_mapping_light.ts @@ -8,7 +8,7 @@ import type { ShortCodesToEmojiDataKey, } from 'virtual:mastodon-emoji-compressed'; -import { unicodeToFilename } from './unicode_to_filename'; +import { unicodeToFilename } from './unicode_utils'; type UnicodeMapping = Record< FilenameData[number][0], diff --git a/app/javascript/mastodon/features/emoji/emoji_utils.js b/app/javascript/mastodon/features/emoji/emoji_utils.js index c13d250567..75b2acafa5 100644 --- a/app/javascript/mastodon/features/emoji/emoji_utils.js +++ b/app/javascript/mastodon/features/emoji/emoji_utils.js @@ -209,50 +209,9 @@ function intersect(a, b) { return uniqA.filter(item => uniqB.indexOf(item) >= 0); } -function deepMerge(a, b) { - let o = {}; - - for (let key in a) { - let originalValue = a[key], - value = originalValue; - - if (Object.hasOwn(b, key)) { - value = b[key]; - } - - if (typeof value === 'object') { - value = deepMerge(originalValue, value); - } - - o[key] = value; - } - - return o; -} - -// https://github.com/sonicdoe/measure-scrollbar -function measureScrollbar() { - const div = document.createElement('div'); - - div.style.width = '100px'; - div.style.height = '100px'; - div.style.overflow = 'scroll'; - div.style.position = 'absolute'; - div.style.top = '-9999px'; - - document.body.appendChild(div); - const scrollbarWidth = div.offsetWidth - div.clientWidth; - document.body.removeChild(div); - - return scrollbarWidth; -} - export { getData, getSanitizedData, uniq, intersect, - deepMerge, - unifiedToNative, - measureScrollbar, }; diff --git a/app/javascript/mastodon/features/emoji/handlers.ts b/app/javascript/mastodon/features/emoji/handlers.ts deleted file mode 100644 index 3b02028f3c..0000000000 --- a/app/javascript/mastodon/features/emoji/handlers.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { autoPlayGif } from '@/mastodon/initial_state'; - -const PARENT_MAX_DEPTH = 10; - -export function handleAnimateGif(event: MouseEvent) { - // We already check this in ui/index.jsx, but just to be sure. - if (autoPlayGif) { - return; - } - - const { target, type } = event; - const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate. - - if (target instanceof HTMLImageElement) { - setAnimateGif(target, animate); - } else if (!(target instanceof HTMLElement) || target === document.body) { - return; - } - - let parent: HTMLElement | null = null; - let iter = 0; - - if (target.classList.contains('animate-parent')) { - parent = target; - } else { - // Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'. - let current: HTMLElement | null = target; - while (current) { - if (iter >= PARENT_MAX_DEPTH) { - return; // We can just exit right now. - } - current = current.parentElement; - if (current?.classList.contains('animate-parent')) { - parent = current; - break; - } - iter++; - } - } - - // Affect all animated children within the parent. - if (parent) { - const animatedChildren = - parent.querySelectorAll('img.custom-emoji'); - for (const child of animatedChildren) { - setAnimateGif(child, animate); - } - } -} - -function setAnimateGif(image: HTMLImageElement, animate: boolean) { - const { classList, dataset } = image; - if ( - !classList.contains('custom-emoji') || - !dataset.static || - !dataset.original - ) { - return; - } - image.src = animate ? dataset.original : dataset.static; -} diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts index 3701ad6767..11ee26aac2 100644 --- a/app/javascript/mastodon/features/emoji/index.ts +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -1,8 +1,9 @@ import { initialState } from '@/mastodon/initial_state'; -import { loadWorker } from '@/mastodon/utils/workers'; import { toSupportedLocale } from './locale'; import { emojiLogger } from './utils'; +// eslint-disable-next-line import/default -- Importing via worker loader. +import EmojiWorker from './worker?worker&inline'; const userLocale = toSupportedLocale(initialState?.meta.locale ?? 'en'); @@ -16,9 +17,7 @@ export function initializeEmoji() { log('initializing emojis'); if (!worker && 'Worker' in window) { try { - worker = loadWorker(new URL('./worker', import.meta.url), { - type: 'module', - }); + worker = new EmojiWorker(); } catch (err) { console.warn('Error creating web worker:', err); } diff --git a/app/javascript/mastodon/features/emoji/unicode_to_filename.js b/app/javascript/mastodon/features/emoji/unicode_to_filename.js deleted file mode 100644 index cfe5539c7b..0000000000 --- a/app/javascript/mastodon/features/emoji/unicode_to_filename.js +++ /dev/null @@ -1,26 +0,0 @@ -// taken from: -// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 -export const unicodeToFilename = (str) => { - let result = ''; - let charCode = 0; - let p = 0; - let i = 0; - while (i < str.length) { - charCode = str.charCodeAt(i++); - if (p) { - if (result.length > 0) { - result += '-'; - } - result += (0x10000 + ((p - 0xD800) << 10) + (charCode - 0xDC00)).toString(16); - p = 0; - } else if (0xD800 <= charCode && charCode <= 0xDBFF) { - p = charCode; - } else { - if (result.length > 0) { - result += '-'; - } - result += charCode.toString(16); - } - } - return result; -}; diff --git a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js b/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js deleted file mode 100644 index 15f60aa7c3..0000000000 --- a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js +++ /dev/null @@ -1,21 +0,0 @@ -function padLeft(str, num) { - while (str.length < num) { - str = '0' + str; - } - - return str; -} - -export const unicodeToUnifiedName = (str) => { - let output = ''; - - for (let i = 0; i < str.length; i += 2) { - if (i > 0) { - output += '-'; - } - - output += padLeft(str.codePointAt(i).toString(16).toUpperCase(), 4); - } - - return output; -}; diff --git a/app/javascript/mastodon/features/emoji/unicode_utils.ts b/app/javascript/mastodon/features/emoji/unicode_utils.ts new file mode 100644 index 0000000000..04175ee9f9 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/unicode_utils.ts @@ -0,0 +1,43 @@ +// taken from: +// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 +export function unicodeToFilename(str: string) { + let result = ''; + let charCode = 0; + let p = 0; + let i = 0; + while (i < str.length) { + charCode = str.charCodeAt(i++); + if (p) { + if (result.length > 0) { + result += '-'; + } + result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString( + 16, + ); + p = 0; + } else if (0xd800 <= charCode && charCode <= 0xdbff) { + p = charCode; + } else { + if (result.length > 0) { + result += '-'; + } + result += charCode.toString(16); + } + } + return result; +} + +export function unicodeToUnifiedName(str: string) { + let output = ''; + + for (let i = 0; i < str.length; i += 2) { + if (i > 0) { + output += '-'; + } + + output += + str.codePointAt(i)?.toString(16).toUpperCase().padStart(4, '0') ?? ''; + } + + return output; +} diff --git a/app/javascript/mastodon/features/firehose/index.jsx b/app/javascript/mastodon/features/firehose/index.jsx index 91704f1234..ca3dd7ce38 100644 --- a/app/javascript/mastodon/features/firehose/index.jsx +++ b/app/javascript/mastodon/features/firehose/index.jsx @@ -24,6 +24,14 @@ import StatusListContainer from '../ui/containers/status_list_container'; const messages = defineMessages({ title: { id: 'column.firehose', defaultMessage: 'Live feeds' }, + title_local: { + id: 'column.firehose_local', + defaultMessage: 'Live feed for this server', + }, + title_singular: { + id: 'column.firehose_singular', + defaultMessage: 'Live feed', + }, }); const ColumnSettings = () => { @@ -161,13 +169,23 @@ const Firehose = ({ feedType, multiColumn }) => { /> ); + let title; + + if (canViewFeed(signedIn, permissions, localLiveFeedAccess) && canViewFeed(signedIn, permissions, remoteLiveFeedAccess)) { + title = messages.title; + } else if (canViewFeed(signedIn, permissions, localLiveFeedAccess)) { + title = messages.title_local; + } else { + title = messages.title_singular; + } + return ( { - this.node = c; - }; - - componentDidMount () { - this._updateLinks(); - } - - componentDidUpdate () { - this._updateLinks(); - } - - _updateLinks () { - const node = this.node; - - if (!node) { - return; - } - - const links = node.querySelectorAll('a'); - - for (var i = 0; i < links.length; ++i) { - let link = links[i]; - - if (link.classList.contains('status-link')) { - continue; - } - - link.classList.add('status-link'); - - let mention = this.props.announcement.get('mentions').find(item => link.href === item.get('url')); - - if (mention) { - link.addEventListener('click', this.onMentionClick.bind(this, mention), false); - link.setAttribute('title', mention.get('acct')); - } else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { - link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false); - } else { - let status = this.props.announcement.get('statuses').find(item => link.href === item.get('url')); - if (status) { - link.addEventListener('click', this.onStatusClick.bind(this, status), false); - } - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener'); - } - } - - onMentionClick = (mention, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${mention.get('acct')}`); - } - }; - - onHashtagClick = (hashtag, e) => { - hashtag = hashtag.replace(/^#/, ''); - - if (this.props.history&& e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/tags/${hashtag}`); - } - }; - - onStatusClick = (status, e) => { - if (this.props.history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); - } - }; - - render () { - const { announcement } = this.props; - - return ( -
- ); - } - -} - -const Content = withRouter(ContentWithRouter); - -class Emoji extends PureComponent { - - static propTypes = { - emoji: PropTypes.string.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - hovered: PropTypes.bool.isRequired, - }; - - render () { - const { emoji, emojiMap, hovered } = this.props; - - if (unicodeMapping[emoji]) { - const { filename, shortCode } = unicodeMapping[this.props.emoji]; - const title = shortCode ? `:${shortCode}:` : ''; - - return ( - {emoji} - ); - } else if (emojiMap.get(emoji)) { - const filename = (autoPlayGif || hovered) ? emojiMap.getIn([emoji, 'url']) : emojiMap.getIn([emoji, 'static_url']); - const shortCode = `:${emoji}:`; - - return ( - {shortCode} - ); - } else { - return null; - } - } - -} - -class Reaction extends ImmutablePureComponent { - - static propTypes = { - announcementId: PropTypes.string.isRequired, - reaction: ImmutablePropTypes.map.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - style: PropTypes.object, - }; - - state = { - hovered: false, - }; - - handleClick = () => { - const { reaction, announcementId, addReaction, removeReaction } = this.props; - - if (reaction.get('me')) { - removeReaction(announcementId, reaction.get('name')); - } else { - addReaction(announcementId, reaction.get('name')); - } - }; - - handleMouseEnter = () => this.setState({ hovered: true }); - - handleMouseLeave = () => this.setState({ hovered: false }); - - render () { - const { reaction } = this.props; - - let shortCode = reaction.get('name'); - - if (unicodeMapping[shortCode]) { - shortCode = unicodeMapping[shortCode].shortCode; - } - - return ( - - - - - - - - - ); - } - -} - -const ReactionsBar = ({ - announcementId, - reactions, - emojiMap, - addReaction, - removeReaction, -}) => { - const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]); - - const handleEmojiPick = useCallback((emoji) => { - addReaction(announcementId, emoji.native.replaceAll(/:/g, '')); - }, [addReaction, announcementId]); - - const transitions = useTransition(visibleReactions, { - from: { - scale: 0, - }, - enter: { - scale: 1, - }, - leave: { - scale: 0, - }, - keys: visibleReactions.map(x => x.get('name')), - }); - - return ( -
- {transitions(({ scale }, reaction) => ( - `scale(${s})`) }} - addReaction={addReaction} - removeReaction={removeReaction} - announcementId={announcementId} - emojiMap={emojiMap} - /> - ))} - - {visibleReactions.length < 8 && ( - } - /> - )} -
- ); -}; -ReactionsBar.propTypes = { - announcementId: PropTypes.string.isRequired, - reactions: ImmutablePropTypes.list.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, -}; - -class Announcement extends ImmutablePureComponent { - - static propTypes = { - announcement: ImmutablePropTypes.map.isRequired, - emojiMap: ImmutablePropTypes.map.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - selected: PropTypes.bool, - }; - - state = { - unread: !this.props.announcement.get('read'), - }; - - componentDidUpdate () { - const { selected, announcement } = this.props; - if (!selected && this.state.unread !== !announcement.get('read')) { - this.setState({ unread: !announcement.get('read') }); - } - } - - render () { - const { announcement } = this.props; - const { unread } = this.state; - const startsAt = announcement.get('starts_at') && new Date(announcement.get('starts_at')); - const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at')); - const now = new Date(); - const hasTimeRange = startsAt && endsAt; - const skipTime = announcement.get('all_day'); - - let timestamp = null; - if (hasTimeRange) { - const skipYear = startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear(); - const skipEndDate = startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear(); - timestamp = ( - <> - - - - ); - } else { - const publishedAt = new Date(announcement.get('published_at')); - timestamp = ( - - ); - } - - return ( -
- - - · {timestamp} - - - - - - - {unread && } -
- ); - } - -} - -class Announcements extends ImmutablePureComponent { - - static propTypes = { - announcements: ImmutablePropTypes.list, - emojiMap: ImmutablePropTypes.map.isRequired, - dismissAnnouncement: PropTypes.func.isRequired, - addReaction: PropTypes.func.isRequired, - removeReaction: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - state = { - index: 0, - }; - - static getDerivedStateFromProps(props, state) { - if (props.announcements.size > 0 && state.index >= props.announcements.size) { - return { index: props.announcements.size - 1 }; - } else { - return null; - } - } - - componentDidMount () { - this._markAnnouncementAsRead(); - } - - componentDidUpdate () { - this._markAnnouncementAsRead(); - } - - _markAnnouncementAsRead () { - const { dismissAnnouncement, announcements } = this.props; - const { index } = this.state; - const announcement = announcements.get(announcements.size - 1 - index); - if (!announcement.get('read')) dismissAnnouncement(announcement.get('id')); - } - - handleChangeIndex = index => { - this.setState({ index: index % this.props.announcements.size }); - }; - - handleNextClick = () => { - this.setState({ index: (this.state.index + 1) % this.props.announcements.size }); - }; - - handlePrevClick = () => { - this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size }); - }; - - render () { - const { announcements, intl } = this.props; - const { index } = this.state; - - if (announcements.isEmpty()) { - return null; - } - - return ( -
- - -
- - {announcements.map((announcement, idx) => ( - - )).reverse()} - - - {announcements.size > 1 && ( -
- - {index + 1} / {announcements.size} - -
- )} -
-
- ); - } - -} - -export default injectIntl(Announcements); diff --git a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js b/app/javascript/mastodon/features/getting_started/containers/announcements_container.js deleted file mode 100644 index 3bb1b8e8d1..0000000000 --- a/app/javascript/mastodon/features/getting_started/containers/announcements_container.js +++ /dev/null @@ -1,23 +0,0 @@ -import { createSelector } from '@reduxjs/toolkit'; -import { Map as ImmutableMap } from 'immutable'; -import { connect } from 'react-redux'; - - -import { addReaction, removeReaction, dismissAnnouncement } from 'mastodon/actions/announcements'; - -import Announcements from '../components/announcements'; - -const customEmojiMap = createSelector([state => state.get('custom_emojis')], items => items.reduce((map, emoji) => map.set(emoji.get('shortcode'), emoji), ImmutableMap())); - -const mapStateToProps = state => ({ - announcements: state.getIn(['announcements', 'items']), - emojiMap: customEmojiMap(state), -}); - -const mapDispatchToProps = dispatch => ({ - dismissAnnouncement: id => dispatch(dismissAnnouncement(id)), - addReaction: (id, name) => dispatch(addReaction(id, name)), - removeReaction: (id, name) => dispatch(removeReaction(id, name)), -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Announcements); diff --git a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx index 8c7c704849..335e0f1a38 100644 --- a/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx +++ b/app/javascript/mastodon/features/home_timeline/components/announcements/index.tsx @@ -10,10 +10,8 @@ import ReactSwipeableViews from 'react-swipeable-views'; import elephantUIPlane from '@/images/elephant_ui_plane.svg'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; import { IconButton } from '@/mastodon/components/icon_button'; -import LegacyAnnouncements from '@/mastodon/features/getting_started/containers/announcements_container'; import { mascot, reduceMotion } from '@/mastodon/initial_state'; import { createAppSelector, useAppSelector } from '@/mastodon/store'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react'; import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'; @@ -32,7 +30,7 @@ const announcementSelector = createAppSelector( (announcements.get('items')?.toJS() as IAnnouncement[] | undefined) ?? [], ); -export const ModernAnnouncements: FC = () => { +export const Announcements: FC = () => { const intl = useIntl(); const announcements = useAppSelector(announcementSelector); @@ -112,7 +110,3 @@ export const ModernAnnouncements: FC = () => {
); }; - -export const Announcements = isModernEmojiEnabled() - ? ModernAnnouncements - : LegacyAnnouncements; diff --git a/app/javascript/mastodon/features/navigation_panel/index.tsx b/app/javascript/mastodon/features/navigation_panel/index.tsx index 446deb1dd6..5b5af7a4e5 100644 --- a/app/javascript/mastodon/features/navigation_panel/index.tsx +++ b/app/javascript/mastodon/features/navigation_panel/index.tsx @@ -61,6 +61,10 @@ const messages = defineMessages({ }, explore: { id: 'explore.title', defaultMessage: 'Trending' }, firehose: { id: 'column.firehose', defaultMessage: 'Live feeds' }, + firehose_singular: { + id: 'column.firehose_singular', + defaultMessage: 'Live feed', + }, direct: { id: 'navigation_bar.direct', defaultMessage: 'Private mentions' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, @@ -275,7 +279,12 @@ export const NavigationPanel: React.FC<{ multiColumn?: boolean }> = ({ icon='globe' iconComponent={PublicIcon} isActive={isFirehoseActive} - text={intl.formatMessage(messages.firehose)} + text={intl.formatMessage( + canViewFeed(signedIn, permissions, localLiveFeedAccess) && + canViewFeed(signedIn, permissions, remoteLiveFeedAccess) + ? messages.firehose + : messages.firehose_singular, + )} /> )} diff --git a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx index 1d1b684b80..9e7f66d112 100644 --- a/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx +++ b/app/javascript/mastodon/features/notifications_v2/components/embedded_status_content.tsx @@ -1,47 +1,17 @@ import { useCallback, useMemo } from 'react'; -import { useHistory } from 'react-router-dom'; - import type { List } from 'immutable'; -import type { History } from 'history'; - -import type { ApiMentionJSON } from '@/mastodon/api_types/statuses'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import type { Status } from '@/mastodon/models/status'; -import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import type { Mention } from './embedded_status'; -const handleMentionClick = ( - history: History, - mention: ApiMentionJSON, - e: MouseEvent, -) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - history.push(`/@${mention.acct}`); - } -}; - -const handleHashtagClick = ( - history: History, - hashtag: string, - e: MouseEvent, -) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - history.push(`/tags/${hashtag.replace(/^#/, '')}`); - } -}; - export const EmbeddedStatusContent: React.FC<{ status: Status; className?: string; }> = ({ status, className }) => { - const history = useHistory(); - const mentions = useMemo( () => (status.get('mentions') as List).toJS(), [status], @@ -57,55 +27,10 @@ export const EmbeddedStatusContent: React.FC<{ hrefToMention, }); - const handleContentRef = useCallback( - (node: HTMLDivElement | null) => { - if (!node || isModernEmojiEnabled()) { - return; - } - - const links = node.querySelectorAll('a'); - - for (const link of links) { - if (link.classList.contains('status-link')) { - continue; - } - - link.classList.add('status-link'); - - const mention = mentions.find((item) => link.href === item.url); - - if (mention) { - link.addEventListener( - 'click', - handleMentionClick.bind(null, history, mention), - false, - ); - link.setAttribute('title', `@${mention.acct}`); - link.setAttribute('href', `/@${mention.acct}`); - } else if ( - link.textContent.startsWith('#') || - link.previousSibling?.textContent?.endsWith('#') - ) { - link.addEventListener( - 'click', - handleHashtagClick.bind(null, history, link.text), - false, - ); - link.setAttribute('href', `/tags/${link.text.replace(/^#/, '')}`); - } else { - link.setAttribute('title', link.href); - link.classList.add('unhandled-link'); - } - } - }, - [mentions, history], - ); - return ( diff --git a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx index f260444265..ae4c4ed4f7 100644 --- a/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx +++ b/app/javascript/mastodon/features/ui/components/compare_history_modal.jsx @@ -14,7 +14,6 @@ import { IconButton } from 'mastodon/components/icon_button'; import InlineAccount from 'mastodon/components/inline_account'; import MediaAttachments from 'mastodon/components/media_attachments'; import { RelativeTimestamp } from 'mastodon/components/relative_timestamp'; -import emojify from 'mastodon/features/emoji/emoji'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { CustomEmojiProvider } from '@/mastodon/components/emoji/context'; @@ -48,13 +47,8 @@ class CompareHistoryModal extends PureComponent { const { index, versions, language, onClose } = this.props; const currentVersion = versions.get(index); - const emojiMap = currentVersion.get('emojis').reduce((obj, emoji) => { - obj[`:${emoji.get('shortcode')}:`] = emoji.toJS(); - return obj; - }, {}); - - const content = emojify(currentVersion.get('content'), emojiMap); - const spoilerContent = emojify(escapeTextContentForBrowser(currentVersion.get('spoiler_text')), emojiMap); + const content = currentVersion.get('content'); + const spoilerContent = escapeTextContentForBrowser(currentVersion.get('spoiler_text')); const formattedDate = ; const formattedName = ; @@ -99,7 +93,7 @@ class CompareHistoryModal extends PureComponent { diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index f53870a314..209e4b4a87 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -22,12 +22,11 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex import { layoutFromWindow } from 'mastodon/is_mobile'; import { WithRouterPropTypes } from 'mastodon/utils/react_router'; -import { handleAnimateGif } from '../emoji/handlers'; import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose'; import { clearHeight } from '../../actions/height_cache'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { expandHomeTimeline } from '../../actions/timelines'; -import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards, autoPlayGif } from '../../initial_state'; +import { initialState, me, owner, singleUserMode, trendsEnabled, landingPage, localLiveFeedAccess, disableHoverCards } from '../../initial_state'; import BundleColumnError from './components/bundle_column_error'; import { NavigationBar } from './components/navigation_bar'; @@ -382,11 +381,6 @@ class UI extends PureComponent { window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); - if (!autoPlayGif) { - window.addEventListener('mouseover', handleAnimateGif, { passive: true }); - window.addEventListener('mouseout', handleAnimateGif, { passive: true }); - } - document.addEventListener('dragenter', this.handleDragEnter, false); document.addEventListener('dragover', this.handleDragOver, false); document.addEventListener('drop', this.handleDrop, false); @@ -412,8 +406,6 @@ class UI extends PureComponent { window.removeEventListener('blur', this.handleWindowBlur); window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); - window.removeEventListener('mouseover', handleAnimateGif); - window.removeEventListener('mouseout', handleAnimateGif); document.removeEventListener('dragenter', this.handleDragEnter); document.removeEventListener('dragover', this.handleDragOver); diff --git a/app/javascript/mastodon/hooks/useLinks.ts b/app/javascript/mastodon/hooks/useLinks.ts deleted file mode 100644 index 77609181be..0000000000 --- a/app/javascript/mastodon/hooks/useLinks.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { useCallback } from 'react'; - -import { useHistory } from 'react-router-dom'; - -import { isFulfilled, isRejected } from '@reduxjs/toolkit'; - -import { openURL } from 'mastodon/actions/search'; -import { useAppDispatch } from 'mastodon/store'; - -import { isModernEmojiEnabled } from '../utils/environment'; - -const isMentionClick = (element: HTMLAnchorElement) => - element.classList.contains('mention') && - !element.classList.contains('hashtag'); - -const isHashtagClick = (element: HTMLAnchorElement) => - element.textContent.startsWith('#') || - element.previousSibling?.textContent?.endsWith('#'); - -export const useLinks = (skipHashtags?: boolean) => { - const history = useHistory(); - const dispatch = useAppDispatch(); - - const handleHashtagClick = useCallback( - (element: HTMLAnchorElement) => { - const { textContent } = element; - - if (!textContent) return; - - history.push(`/tags/${textContent.replace(/^#/, '')}`); - }, - [history], - ); - - const handleMentionClick = useCallback( - async (element: HTMLAnchorElement) => { - const result = await dispatch(openURL({ url: element.href })); - - if (isFulfilled(result)) { - if (result.payload.accounts[0]) { - history.push(`/@${result.payload.accounts[0].acct}`); - } else if (result.payload.statuses[0]) { - history.push( - `/@${result.payload.statuses[0].account.acct}/${result.payload.statuses[0].id}`, - ); - } else { - window.location.href = element.href; - } - } else if (isRejected(result)) { - window.location.href = element.href; - } - }, - [dispatch, history], - ); - - const handleClick = useCallback( - (e: React.MouseEvent) => { - // Exit early if modern emoji is enabled, as this is handled by HandledLink. - if (isModernEmojiEnabled()) { - return; - } - - const target = (e.target as HTMLElement).closest('a'); - - if (!target || e.button !== 0 || e.ctrlKey || e.metaKey) { - return; - } - - if (isMentionClick(target)) { - e.preventDefault(); - void handleMentionClick(target); - } else if (isHashtagClick(target) && !skipHashtags) { - e.preventDefault(); - handleHashtagClick(target); - } - }, - [skipHashtags, handleMentionClick, handleHashtagClick], - ); - - return handleClick; -}; diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 6025639884..c7e102aacd 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Спиране на известяване при публикуване от @{name}", "account.domain_blocking": "Блокиране на домейн", "account.edit_profile": "Редактиране на профила", + "account.edit_profile_short": "Редактиране", "account.enable_notifications": "Известяване при публикуване от @{name}", "account.endorse": "Представи в профила", "account.familiar_followers_many": "Последвано от {name1}, {name2}, и {othersCount, plural, one {един друг, когото познавате} other {# други, които познавате}}", @@ -40,6 +41,9 @@ "account.featured_tags.last_status_never": "Няма публикации", "account.follow": "Последване", "account.follow_back": "Последване взаимно", + "account.follow_request_cancel": "Отказване на заявката", + "account.follow_request_cancel_short": "Отказ", + "account.follow_request_short": "Заявка", "account.followers": "Последователи", "account.followers.empty": "Още никой не следва потребителя.", "account.followers_counter": "{count, plural, one {{counter} последовател} other {{counter} последователи}}", @@ -238,6 +242,9 @@ "confirmations.missing_alt_text.secondary": "Все пак да се публикува", "confirmations.missing_alt_text.title": "Добавяте ли алтернативен текст?", "confirmations.mute.confirm": "Заглушаване", + "confirmations.quiet_post_quote_info.dismiss": "Без друго напомняне", + "confirmations.quiet_post_quote_info.got_it": "Схванах", + "confirmations.quiet_post_quote_info.title": "Цитиране на публикации за тиха публика", "confirmations.redraft.confirm": "Изтриване и преработване", "confirmations.redraft.message": "Наистина ли искате да изтриете тази публикация и да я направите чернова? Означаванията като любими и подсилванията ще се изгубят, а и отговорите към първоначалната публикация ще осиротеят.", "confirmations.redraft.title": "Изтривате и преработвате ли публикацията?", @@ -247,7 +254,11 @@ "confirmations.revoke_quote.confirm": "Премахване на публикация", "confirmations.revoke_quote.message": "Действието е неотменимо.", "confirmations.revoke_quote.title": "Премахвате ли публикацията?", + "confirmations.unblock.confirm": "Отблокиране", + "confirmations.unblock.title": "Отблокирате ли @{name}?", "confirmations.unfollow.confirm": "Без следване", + "confirmations.unfollow.title": "Спирате ли следване на {name}?", + "confirmations.withdraw_request.confirm": "Оттегляне на заявката", "content_warning.hide": "Скриване на публ.", "content_warning.show": "Нека се покаже", "content_warning.show_more": "Показване на още", @@ -442,10 +453,12 @@ "ignore_notifications_modal.private_mentions_title": "Пренебрегвате ли известия от непоискани лични споменавания?", "info_button.label": "Помощ", "info_button.what_is_alt_text": "

Какво е алтернативен текст?

Алтернативният текст осигурява описания на изображение за хора със зрителни увреждания, връзки с ниска честотна лента или търсещите допълнителен контекст.

Може да подобрите достъпността и разбираемостта за всеки, пишейки ясен, кратък и обективен алтернативен текст.

  • Уловете важните елементи
  • Обобщете текста в образите
  • Употребявайте правилна структура на изречението
  • Избягвайте излишна информация
  • Съсредоточете се върху тенденциите и ключови констатации в сложни онагледявания (като диаграми и карти)
", + "interaction_modal.action": "Трябва да влезете с акаунта си, в който и да е сървър на Mastodon, когото използвате, за да взаимодействате с публикация на {name}.", "interaction_modal.go": "Напред", "interaction_modal.no_account_yet": "Още ли нямате акаунт?", "interaction_modal.on_another_server": "На различен сървър", "interaction_modal.on_this_server": "На този сървър", + "interaction_modal.title": "Влезте, за да продължите", "interaction_modal.username_prompt": "Напр. {example}", "intervals.full.days": "{number, plural, one {# ден} other {# дни}}", "intervals.full.hours": "{number, plural, one {# час} other {# часа}}", @@ -596,6 +609,7 @@ "notification.moderation_warning.action_suspend": "Вашият акаунт е спрян.", "notification.own_poll": "Анкетата ви приключи", "notification.poll": "Анкета, в която гласувахте, приключи", + "notification.quoted_update": "{name} редактира публикация, която цитирахте", "notification.reblog": "{name} подсили ваша публикация", "notification.reblog.name_and_others_with_link": "{name} и {count, plural, one {# друг} other {# други}} подсилиха ваша публикация", "notification.relationships_severance_event": "Изгуби се връзката с {name}", @@ -715,10 +729,17 @@ "privacy.private.short": "Последователи", "privacy.public.long": "Всеки във и извън Mastodon", "privacy.public.short": "Публично", + "privacy.quote.anyone": "{visibility}, всеки може да цитира", + "privacy.quote.disabled": "{visibility}, цитатите са изключени", + "privacy.quote.limited": "{visibility}, цитатите са ограничени", "privacy.unlisted.additional": "Това действие е точно като публичното, с изключение на това, че публикацията няма да се появява в каналите на живо, хаштаговете, разглеждането или търсенето в Mastodon, дори ако сте избрали да се публично видими на ниво акаунт.", "privacy.unlisted.short": "Тиха публика", "privacy_policy.last_updated": "Последно осъвременяване на {date}", "privacy_policy.title": "Политика за поверителност", + "quote_error.edit": "Не може да се добавят цитати, редайтирайки публикация.", + "quote_error.poll": "Не може да се цитира при анкетиране.", + "quote_error.unauthorized": "Нямате право да цитирате тази публикация.", + "quote_error.upload": "Цитирането не е позволено с мултимедийни прикачвания.", "recommended": "Препоръчано", "refresh": "Опресняване", "regeneration_indicator.please_stand_by": "Изчакайте.", @@ -734,6 +755,8 @@ "relative_time.minutes": "{number}м.", "relative_time.seconds": "{number}с.", "relative_time.today": "днес", + "remove_quote_hint.button_label": "Схванах", + "remove_quote_hint.message": "Може да го направите от менюто възможности {icon}.", "reply_indicator.attachments": "{count, plural, one {# прикаване} other {# прикачвания}}", "reply_indicator.cancel": "Отказ", "reply_indicator.poll": "Анкета", @@ -825,13 +848,22 @@ "status.admin_account": "Отваряне на интерфейс за модериране за @{name}", "status.admin_domain": "Отваряне на модериращия интерфейс за {domain}", "status.admin_status": "Отваряне на публикацията в модериращия интерфейс", + "status.all_disabled": "Подсилването и цитатите са изключени", "status.block": "Блокиране на @{name}", "status.bookmark": "Отмятане", "status.cancel_reblog_private": "Край на подсилването", + "status.cannot_quote": "Не е позволено да цитирате тази публикация", "status.cannot_reblog": "Публикацията не може да се подсилва", + "status.context.loading": "Зареждане на още отговори", + "status.context.loading_error": "Не можаха да се заредят нови отговори", + "status.context.loading_success": "Новите отговори заредени", + "status.context.more_replies_found": "Още намерени отговори", + "status.context.retry": "Друг опит", + "status.context.show": "Показване", "status.continued_thread": "Продължена нишка", "status.copy": "Копиране на връзката към публикация", "status.delete": "Изтриване", + "status.delete.success": "Публикацията е изтрита", "status.detailed_status": "Подробен изглед на разговора", "status.direct": "Частно споменаване на @{name}", "status.direct_indicator": "Частно споменаване", @@ -855,23 +887,32 @@ "status.open": "Разширяване на публикацията", "status.pin": "Закачане в профила", "status.quote_error.filtered": "Скрито поради един от филтрите ви", + "status.quote_error.limited_account_hint.title": "Този акаунт е бил скрит от модераторите на {domain}.", "status.quote_error.not_available": "Неналична публикация", "status.quote_error.pending_approval": "Публикацията чака одобрение", + "status.quote_error.revoked": "Премахната публикация от автора", + "status.quote_followers_only": "Само последователи могат да цитират тази публикация", + "status.quote_manual_review": "Авторът ще преглежда ръчно", "status.quote_policy_change": "Промяна кой може да цитира", "status.quote_post_author": "Цитирах публикация от @{name}", + "status.quote_private": "Частните публикации не може да се цитират", "status.read_more": "Още за четене", "status.reblog": "Подсилване", + "status.reblog_or_quote": "Подсилване или цитиране", + "status.reblog_private": "Споделете пак с последователите си", "status.reblogged_by": "{name} подсили", "status.reblogs": "{count, plural, one {подсилване} other {подсилвания}}", "status.reblogs.empty": "Още никого не е подсилвал публикацията. Подсилващият ще се покаже тук.", "status.redraft": "Изтриване и преработване", "status.remove_bookmark": "Премахване на отметката", "status.remove_favourite": "Премахване от любими", + "status.remove_quote": "Премахване", "status.replied_in_thread": "Отговорено в нишката", "status.replied_to": "В отговор до {name}", "status.reply": "Отговор", "status.replyAll": "Отговор на нишка", "status.report": "Докладване на @{name}", + "status.request_quote": "Заявка за цитиране", "status.revoke_quote": "Премахване на моя публикация от публикацията на @{name}", "status.sensitive_warning": "Деликатно съдържание", "status.share": "Споделяне", @@ -910,6 +951,7 @@ "upload_button.label": "Добавете файл с образ, видео или звук", "upload_error.limit": "Превишено ограничението за качване на файлове.", "upload_error.poll": "Качването на файлове не е позволено с анкети.", + "upload_error.quote": "Цитирайки, не може да качвате файл.", "upload_form.drag_and_drop.instructions": "Натиснете интервал или enter, за да подберете мултимедийно прикачване. Провлачвайки, ползвайте клавишите със стрелки, за да премествате мултимедията във всяка дадена посока. Натиснете пак интервал или enter, за да се стовари мултимедийното прикачване в новото си положение или натиснете Esc за отмяна.", "upload_form.drag_and_drop.on_drag_cancel": "Провлачването е отменено. Мултимедийното прикачване {item} е спуснато.", "upload_form.drag_and_drop.on_drag_end": "Мултимедийното прикачване {item} е спуснато.", @@ -935,6 +977,11 @@ "video.volume_up": "Увеличаване на звука", "visibility_modal.button_title": "Задаване на видимост", "visibility_modal.header": "Видимост и взаимодействие", + "visibility_modal.helper.privacy_editing": "Видимостта не може да се променя след публикуване на публикацията.", + "visibility_modal.privacy_label": "Видимост", "visibility_modal.quote_followers": "Само последователи", - "visibility_modal.quote_public": "Някой" + "visibility_modal.quote_label": "Кой може да цитира", + "visibility_modal.quote_nobody": "Само аз", + "visibility_modal.quote_public": "Някой", + "visibility_modal.save": "Запазване" } diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 12fb8f434e..f20bae291b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -173,6 +173,8 @@ "column.edit_list": "Edit list", "column.favourites": "Favorites", "column.firehose": "Live feeds", + "column.firehose_local": "Live feed for this server", + "column.firehose_singular": "Live feed", "column.follow_requests": "Follow requests", "column.home": "Home", "column.list_members": "Manage list members", diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index ff190a6ed2..f777e47943 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -333,6 +333,7 @@ "empty_column.bookmarked_statuses": "Järjehoidjatesse pole veel lisatud postitusi. Kui lisad mõne, näed neid siin.", "empty_column.community": "Kohalik ajajoon on tühi. Kirjuta midagi avalikult, et pall veerema ajada!", "empty_column.direct": "Sul pole veel ühtegi privaatset mainimist. Kui saadad või saad mõne, ilmuvad need siin.", + "empty_column.disabled_feed": "See infovoog on serveri peakasutajate poolt välja lülitatud.", "empty_column.domain_blocks": "Siin ei ole veel peidetud domeene.", "empty_column.explore_statuses": "Praegu pole ühtegi trendi. Tule hiljem tagasi!", "empty_column.favourited_statuses": "Pole veel lemmikpostitusi. Kui märgid mõne, näed neid siin.", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index e8357bba4b..3a407e7cb6 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -333,6 +333,7 @@ "empty_column.bookmarked_statuses": "Tú hevur enn einki goymt uppslag. Tú tú goymir eitt uppslag, kemur tað her.", "empty_column.community": "Lokala tíðarlinjan er tóm. Skriva okkurt alment fyri at fáa boltin á rull!", "empty_column.direct": "Tú hevur ongar privatar umrøður enn. Tá tú sendir ella móttekur eina privata umrøðu, so verður hon sjónlig her.", + "empty_column.disabled_feed": "Hendan rásin er gjørd óvirkin av ambætaraumsitarunum hjá tær.", "empty_column.domain_blocks": "Enn eru eingi blokeraði domenir.", "empty_column.explore_statuses": "Einki rák er beint nú. Royn aftur seinni!", "empty_column.favourited_statuses": "Tú hevur ongar yndispostar enn. Tá tú gevur einum posti yndismerki, so sært tú hann her.", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 5d757f30da..0bf6beecdc 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -333,6 +333,7 @@ "empty_column.bookmarked_statuses": "Níl aon phostáil leabharmharcaithe agat fós. Nuair a dhéanann tú leabharmharc, beidh sé le feiceáil anseo.", "empty_column.community": "Tá an amlíne áitiúil folamh. Foilsigh rud éigin go poiblí le tús a chur le cúrsaí!", "empty_column.direct": "Níl aon tagairtí príobháideacha agat fós. Nuair a sheolann tú nó a gheobhaidh tú ceann, beidh sé le feiceáil anseo.", + "empty_column.disabled_feed": "Tá an fotha seo díchumasaithe ag riarthóirí do fhreastalaí.", "empty_column.domain_blocks": "Níl aon fearainn bhactha ann go fóill.", "empty_column.explore_statuses": "Níl rud ar bith ag treochtáil faoi láthair. Tar ar ais ar ball!", "empty_column.favourited_statuses": "Níl aon postálacha is fearr leat fós. Nuair is fearr leat ceann, beidh sé le feiceáil anseo.", diff --git a/app/javascript/mastodon/locales/ia.json b/app/javascript/mastodon/locales/ia.json index cd735bbe92..1b5aaa39bf 100644 --- a/app/javascript/mastodon/locales/ia.json +++ b/app/javascript/mastodon/locales/ia.json @@ -172,7 +172,7 @@ "column.domain_blocks": "Dominios blocate", "column.edit_list": "Modificar lista", "column.favourites": "Favorites", - "column.firehose": "Fluxos in directo", + "column.firehose": "Fluxos in vivo", "column.follow_requests": "Requestas de sequimento", "column.home": "Initio", "column.list_members": "Gerer le membros del lista", @@ -333,6 +333,7 @@ "empty_column.bookmarked_statuses": "Tu non ha ancora messages in marcapaginas. Quando tu adde un message al marcapaginas, illo apparera hic.", "empty_column.community": "Le chronologia local es vacue. Scribe qualcosa public pro poner le cosas in marcha!", "empty_column.direct": "Tu non ha ancora mentiones private. Quando tu invia o recipe un mention, illo apparera hic.", + "empty_column.disabled_feed": "Iste canal ha essite disactivate per le adminsistratores de tu servitor.", "empty_column.domain_blocks": "Il non ha dominios blocate ancora.", "empty_column.explore_statuses": "Il non ha tendentias in iste momento. Reveni plus tarde!", "empty_column.favourited_statuses": "Tu non ha alcun message favorite ancora. Quando tu marca un message como favorite, illo apparera hic.", @@ -460,7 +461,7 @@ "ignore_notifications_modal.not_following_title": "Ignorar notificationes de personas que tu non seque?", "ignore_notifications_modal.private_mentions_title": "Ignorar notificationes de mentiones private non requestate?", "info_button.label": "Adjuta", - "info_button.what_is_alt_text": "

Que es texto alternative?

Le texto alternative forni descriptiones de imagines a personas con impedimentos visual, con connexiones lente, o qui cerca contexto additional.

Tu pote meliorar le accessibilitate e le comprension pro totes scribente un texto alternative clar, concise e objective.

  • Captura le elementos importante
  • Summarisa texto in imagines
  • Usa le structura de phrase normal
  • Evita information redundante
  • In figuras complexe (como diagrammas o mappas), concentra te sur le tendentias e punctos clave
", + "info_button.what_is_alt_text": "

Que es texto alternative?

Le texto alternative forni descriptiones de imagines a personas con impedimentos visual, con connexiones lente a internet, o qui cerca contexto supplementari.

Tu pote meliorar le accessibilitate e le comprension pro totes si tu scribe un texto alternative clar, concise e objective.

  • Captura le elementos importante
  • Summarisa texto in imagines
  • Usa un structura conventional de phrases
  • Evita information redundante
  • In figuras complexe (como diagrammas o mappas), concentra te sur le tendentias e punctos clave
", "interaction_modal.action": "Pro interager con le message de {name}, tu debe acceder a tu conto sur le servitor Mastodon que tu usa.", "interaction_modal.go": "Revenir", "interaction_modal.no_account_yet": "Tu non ha ancora un conto?", @@ -574,8 +575,8 @@ "navigation_bar.follows_and_followers": "Sequites e sequitores", "navigation_bar.import_export": "Importar e exportar", "navigation_bar.lists": "Listas", - "navigation_bar.live_feed_local": "Canal in directo (local)", - "navigation_bar.live_feed_public": "Canal in directo (public)", + "navigation_bar.live_feed_local": "Canal in vivo (local)", + "navigation_bar.live_feed_public": "Canal in vivo (public)", "navigation_bar.logout": "Clauder session", "navigation_bar.moderation": "Moderation", "navigation_bar.more": "Plus", @@ -748,7 +749,7 @@ "privacy.quote.anyone": "{visibility}, omnes pote citar", "privacy.quote.disabled": "{visibility}, citation disactivate", "privacy.quote.limited": "{visibility}, citation limitate", - "privacy.unlisted.additional": "Isto es exactemente como public, excepte que le message non apparera in fluxos in directo, in hashtags, in Explorar, o in le recerca de Mastodon, mesmo si tu ha optate pro render tote le conto discoperibile.", + "privacy.unlisted.additional": "Isto es exactemente como public, excepte que le message non apparera in fluxos in vivo, in hashtags, in Explorar, o in le recerca de Mastodon, mesmo si tu ha optate pro render tote le conto discoperibile.", "privacy.unlisted.long": "Non apparera in le resultatos de recerca, tendentias e chronologias public de Mastodon", "privacy.unlisted.short": "Public, non listate", "privacy_policy.last_updated": "Ultime actualisation {date}", diff --git a/app/javascript/mastodon/locales/lad.json b/app/javascript/mastodon/locales/lad.json index b223288f5f..62ec278e64 100644 --- a/app/javascript/mastodon/locales/lad.json +++ b/app/javascript/mastodon/locales/lad.json @@ -38,6 +38,8 @@ "account.follow": "Sige", "account.follow_back": "Sige tamyen", "account.follow_back_short": "Sige tambyen", + "account.follow_request": "Solisita segirle", + "account.follow_request_cancel": "Anula solisitud", "account.follow_request_cancel_short": "Anula", "account.follow_request_short": "Solisitud", "account.followers": "Suivantes", @@ -62,6 +64,7 @@ "account.mute_short": "Silensia", "account.muted": "Silensiado", "account.muting": "Silensyando", + "account.mutual": "Vos sigesh mutualmente", "account.no_bio": "No ay deskripsion.", "account.open_original_page": "Avre pajina orijnala", "account.posts": "Publikasyones", @@ -97,6 +100,7 @@ "alert.unexpected.title": "Atyo!", "alt_text_badge.title": "Teksto alternativo", "alt_text_modal.add_alt_text": "Adjusta teksto alternativo", + "alt_text_modal.add_text_from_image": "Adjusta teksto de imaje", "alt_text_modal.cancel": "Anula", "alt_text_modal.change_thumbnail": "Troka minyatura", "alt_text_modal.done": "Fecho", @@ -210,6 +214,7 @@ "confirmations.logout.message": "Estas siguro ke keres salir de tu kuento?", "confirmations.logout.title": "Salir?", "confirmations.missing_alt_text.confirm": "Adjusta teksto alternativo", + "confirmations.missing_alt_text.secondary": "Puvlika de todos modos", "confirmations.missing_alt_text.title": "Adjustar teksto alternativo?", "confirmations.mute.confirm": "Silensia", "confirmations.quiet_post_quote_info.got_it": "Entyendo", @@ -382,6 +387,7 @@ "hints.profiles.see_more_followers": "Ve mas suivantes en {domain}", "hints.profiles.see_more_follows": "Ve mas segidos en {domain}", "hints.profiles.see_more_posts": "Ve mas puvlikasyones en {domain}", + "home.column_settings.show_quotes": "Muestra sitas", "home.column_settings.show_reblogs": "Amostra repartajasyones", "home.column_settings.show_replies": "Amostra repuestas", "home.hide_announcements": "Eskonde pregones", @@ -631,6 +637,7 @@ "privacy_policy.title": "Politika de privasita", "recommended": "Rekomendado", "refresh": "Arefreska", + "regeneration_indicator.please_stand_by": "Aspera por favor.", "relative_time.days": "{number} d", "relative_time.full.days": "antes {number, plural, one {# diya} other {# diyas}}", "relative_time.full.hours": "antes {number, plural, one {# ora} other {# oras}}", @@ -733,8 +740,12 @@ "status.bookmark": "Marka", "status.cancel_reblog_private": "No repartaja", "status.cannot_reblog": "Esta publikasyon no se puede repartajar", + "status.contains_quote": "Kontriene sita", + "status.context.loading_success": "Muevas repuestas kargadas", + "status.context.more_replies_found": "Se toparon mas repuestas", "status.context.retry": "Reprova", "status.context.show": "Amostra", + "status.continued_thread": "Kontinuasion del filo", "status.copy": "Kopia atadijo de publikasyon", "status.delete": "Efasa", "status.delete.success": "Puvlikasyon kitada", @@ -760,9 +771,18 @@ "status.pin": "Fiksa en profil", "status.quote": "Sita", "status.quote.cancel": "Anula la sita", + "status.quote_error.limited_account_hint.action": "Amostra entanto", + "status.quote_error.limited_account_hint.title": "Este kuento fue eskondido por los moderadores de {domain}.", + "status.quote_error.not_available": "Puvlikasyon no desponivle", + "status.quote_error.pending_approval": "Puvlikasyon esta asperando", "status.quote_noun": "Sita", + "status.quote_policy_change": "Troka ken puede sitar", + "status.quote_post_author": "Sito una puvlikasyon de @{name}", + "status.quote_private": "No se puede sitar puvlikasyones privadas", + "status.quotes": "{count, plural, one {sita} other {sitas}}", "status.read_more": "Melda mas", "status.reblog": "Repartaja", + "status.reblog_or_quote": "Repartaja o partaja", "status.reblogged_by": "{name} repartajo", "status.reblogs.empty": "Ainda nadie tiene repartajado esta publikasyon. Kuando algien lo aga, se amostrara aki.", "status.redraft": "Efasa i eskrive de muevo", @@ -823,7 +843,12 @@ "video.pause": "Pauza", "video.play": "Reproduze", "video.unmute": "Desilensia", + "visibility_modal.button_title": "Konfigura la vizibilita", + "visibility_modal.header": "Vizibilita i enteraksyon", "visibility_modal.privacy_label": "Vizivilita", "visibility_modal.quote_followers": "Solo suivantes", + "visibility_modal.quote_label": "Ken puede sitar", + "visibility_modal.quote_nobody": "Solo yo", + "visibility_modal.quote_public": "Todos", "visibility_modal.save": "Guadra" } diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 5457e91dba..c80297f67d 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -28,6 +28,7 @@ "account.disable_notifications": "Cancelar notificações de @{name}", "account.domain_blocking": "Bloqueando domínio", "account.edit_profile": "Editar perfil", + "account.edit_profile_short": "Editar", "account.enable_notifications": "Notificar novos toots de @{name}", "account.endorse": "Recomendar", "account.familiar_followers_many": "Seguido por {name1}, {name2}, e {othersCount, plural, one {um outro que você conhece} other {# outros que você conhece}}", @@ -40,6 +41,11 @@ "account.featured_tags.last_status_never": "Sem publicações", "account.follow": "Seguir", "account.follow_back": "Seguir de volta", + "account.follow_back_short": "Seguir de volta", + "account.follow_request": "Pedir para seguir", + "account.follow_request_cancel": "Cancelar solicitação", + "account.follow_request_cancel_short": "Cancelar", + "account.follow_request_short": "Solicitação", "account.followers": "Seguidores", "account.followers.empty": "Nada aqui.", "account.followers_counter": "{count, plural, one {{counter} seguidor} other {{counter} seguidores}}", @@ -240,6 +246,8 @@ "confirmations.mute.confirm": "Silenciar", "confirmations.quiet_post_quote_info.dismiss": "Não me lembrar novamente", "confirmations.quiet_post_quote_info.got_it": "Entendi", + "confirmations.quiet_post_quote_info.message": "Ao citar uma publicação pública silenciosa, sua postagem será oculta das linhas de tempo em tendência.", + "confirmations.quiet_post_quote_info.title": "Citando publicações públicas silenciadas", "confirmations.redraft.confirm": "Excluir e rascunhar", "confirmations.redraft.message": "Você tem certeza de que quer apagar essa postagem e rascunhá-la? Favoritos e impulsos serão perdidos, e respostas à postagem original ficarão órfãs.", "confirmations.redraft.title": "Excluir e rascunhar publicação?", @@ -249,7 +257,12 @@ "confirmations.revoke_quote.confirm": "Remover publicação", "confirmations.revoke_quote.message": "Essa ação não pode ser desfeita.", "confirmations.revoke_quote.title": "Remover publicação?", + "confirmations.unblock.confirm": "Desbloquear", + "confirmations.unblock.title": "Desbloquear {name}?", "confirmations.unfollow.confirm": "Deixar de seguir", + "confirmations.unfollow.title": "Deixar de seguir {name}?", + "confirmations.withdraw_request.confirm": "Retirar solicitação", + "confirmations.withdraw_request.title": "Cancelar solicitação para seguir {name}?", "content_warning.hide": "Ocultar post", "content_warning.show": "Mostrar mesmo assim", "content_warning.show_more": "Mostrar mais", @@ -320,6 +333,7 @@ "empty_column.bookmarked_statuses": "Nada aqui. Quando você salvar um toot, ele aparecerá aqui.", "empty_column.community": "A linha local está vazia. Publique algo para começar!", "empty_column.direct": "Você ainda não tem mensagens privadas. Quando você enviar ou receber uma, será exibida aqui.", + "empty_column.disabled_feed": "Este feed foi desativado pelos administradores do servidor.", "empty_column.domain_blocks": "Nada aqui.", "empty_column.explore_statuses": "Nada está em alta no momento. Volte mais tarde!", "empty_column.favourited_statuses": "Você ainda não tem publicações favoritas. Quanto você marcar uma como favorita, ela aparecerá aqui.", @@ -448,10 +462,12 @@ "ignore_notifications_modal.private_mentions_title": "Ignorar notificações de menções privadas não solicitadas?", "info_button.label": "Ajuda", "info_button.what_is_alt_text": "

O que é texto alternativo?

O texto alternativo fornece descrições de imagens para pessoas com deficiências visuais, conexões de internet de baixa largura de banda ou aquelas que buscam mais contexto.

Você pode melhorar a acessibilidade e a compreensão para todos escrevendo texto alternativo claro, conciso e objetivo.

  • Capture elementos importantes
  • Resuma textos em imagens
  • Use estrutura de frases regular
  • Evite informações redundantes
  • Foque em tendências e descobertas principais em visuais complexos (como diagramas ou mapas)
", + "interaction_modal.action": "Para interagir com o post de {name}, você precisa entrar em sua conta em qualquer servidor Mastodon que você use.", "interaction_modal.go": "Ir", "interaction_modal.no_account_yet": "Não possui uma conta ainda?", "interaction_modal.on_another_server": "Em um servidor diferente", "interaction_modal.on_this_server": "Neste servidor", + "interaction_modal.title": "Faça login para continuar", "interaction_modal.username_prompt": "p. e.x.: {example}", "intervals.full.days": "{number, plural, one {# dia} other {# dias}}", "intervals.full.hours": "{number, plural, one {# hora} other {# horas}}", @@ -734,9 +750,11 @@ "privacy.quote.disabled": "{visibility} Citações desabilitadas", "privacy.quote.limited": "{visibility} Citações limitadas", "privacy.unlisted.additional": "Isso se comporta exatamente como público, exceto que a publicação não aparecerá nos _feeds ao vivo_ ou nas _hashtags_, explorar, ou barra de busca, mesmo que você seja escolhido em toda a conta.", - "privacy.unlisted.short": "Público (silencioso)", + "privacy.unlisted.long": "Oculto para os resultados de pesquisa do Mastodon, tendências e linhas do tempo públicas", + "privacy.unlisted.short": "Público silenciado", "privacy_policy.last_updated": "Atualizado {date}", "privacy_policy.title": "Política de privacidade", + "quote_error.edit": "Citações não podem ser adicionadas durante a edição de uma publicação.", "quote_error.poll": "Citações não permitidas com enquetes.", "quote_error.quote": "Apenas uma citação por vez é permitido.", "quote_error.unauthorized": "Você não é autorizado a citar essa publicação.", @@ -756,6 +774,9 @@ "relative_time.minutes": "{number}m", "relative_time.seconds": "{number}s", "relative_time.today": "hoje", + "remove_quote_hint.button_label": "Entendi", + "remove_quote_hint.message": "Você pode fazê-lo no menu de opções {icon}.", + "remove_quote_hint.title": "Deseja remover sua citação publicada?", "reply_indicator.attachments": "{count, plural, one {# attachment} other {# attachments}}", "reply_indicator.cancel": "Cancelar", "reply_indicator.poll": "Enquete", @@ -851,7 +872,15 @@ "status.block": "Bloquear @{name}", "status.bookmark": "Salvar", "status.cancel_reblog_private": "Desfazer boost", + "status.cannot_quote": "Você não tem permissão para citar esta publicação", "status.cannot_reblog": "Este toot não pode receber boost", + "status.contains_quote": "Contém citação", + "status.context.loading": "Carregando mais respostas", + "status.context.loading_error": "Não foi possível carregar novas respostas", + "status.context.loading_success": "Novas respostas carregadas", + "status.context.more_replies_found": "Mais respostas encontradas", + "status.context.retry": "Tentar novamente", + "status.context.show": "Mostrar", "status.continued_thread": "Continuação da conversa", "status.copy": "Copiar link", "status.delete": "Excluir", @@ -881,24 +910,33 @@ "status.quote": "Citar", "status.quote.cancel": "Cancelar citação", "status.quote_error.filtered": "Oculto devido a um dos seus filtros", + "status.quote_error.limited_account_hint.action": "Mostrar mesmo assim", + "status.quote_error.limited_account_hint.title": "Esta conta foi oculta pelos moderadores do {domain}.", "status.quote_error.not_available": "Publicação indisponível", "status.quote_error.pending_approval": "Publicação pendente", + "status.quote_error.pending_approval_popout.body": "No Mastodon, você pode controlar se alguém pode citar você. Esta publicação está pendente enquanto estamos recebendo a aprovação do autor original.", + "status.quote_error.revoked": "Publicação removida pelo autor", "status.quote_followers_only": "Apenas seguidores podem citar sua publicação", "status.quote_manual_review": "Autor irá revisar manualmente", + "status.quote_noun": "Citar", "status.quote_policy_change": "Mude quem pode citar", "status.quote_post_author": "Publicação citada por @{name}", "status.quote_private": "Publicações privadas não podem ser citadas", "status.quotes": "{count, plural, one {# voto} other {# votos}}", "status.quotes.empty": "Ninguém citou essa publicação até agora. Quando alguém citar aparecerá aqui.", + "status.quotes.local_other_disclaimer": "Citações rejeitadas pelo autor não serão exibidas.", + "status.quotes.remote_other_disclaimer": "Apenas citações do {domain} têm a garantia de serem exibidas aqui. Citações rejeitadas pelo autor não serão exibidas.", "status.read_more": "Ler mais", "status.reblog": "Dar boost", "status.reblog_or_quote": "Acelerar ou citar", + "status.reblog_private": "Compartilhar novamente com seus seguidores", "status.reblogged_by": "{name} deu boost", "status.reblogs": "{count, plural, one {boost} other {boosts}}", "status.reblogs.empty": "Nada aqui. Quando alguém der boost, o usuário aparecerá aqui.", "status.redraft": "Excluir e rascunhar", "status.remove_bookmark": "Remover do Salvos", "status.remove_favourite": "Remover dos favoritos", + "status.remove_quote": "Remover", "status.replied_in_thread": "Respondido na conversa", "status.replied_to": "Em resposta a {name}", "status.reply": "Responder", @@ -970,6 +1008,8 @@ "visibility_modal.button_title": "Selecionar Visibilidade", "visibility_modal.header": "Visibilidade e interação", "visibility_modal.helper.direct_quoting": "Menções privadas escritas no Mastodon.", + "visibility_modal.helper.privacy_editing": "A visibilidade não pode ser alterada após uma publicação ser publicada.", + "visibility_modal.helper.privacy_private_self_quote": "As auto-citações de publicações privadas não podem ser públicas.", "visibility_modal.helper.private_quoting": "Posts somente para seguidores feitos no Mastodon não podem ser citados por outros.", "visibility_modal.helper.unlisted_quoting": "Quando as pessoas citam você, sua publicação também será ocultada das linhas de tempo de tendência.", "visibility_modal.instructions": "Controle quem pode interagir com este post. Você também pode aplicar as configurações para todos os posts futuros navegando para Preferências > Postagem padrão.", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 00181ba8ca..c58e790c69 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -333,6 +333,7 @@ "empty_column.bookmarked_statuses": "Ainda não tem nenhuma publicação salva. Quando salvar uma, ela aparecerá aqui.", "empty_column.community": "A cronologia local está vazia. Escreve algo publicamente para começar!", "empty_column.direct": "Ainda não tens qualquer menção privada. Quando enviares ou receberes uma, ela irá aparecer aqui.", + "empty_column.disabled_feed": "Esta cronologia foi desativada pelos administradores do seu servidor.", "empty_column.domain_blocks": "Ainda não há qualquer domínio bloqueado.", "empty_column.explore_statuses": "Não há nada em destaque neste momento. Volte mais tarde!", "empty_column.favourited_statuses": "Ainda não assinalaste qualquer publicação como favorita. Quando o fizeres, ela aparecerá aqui.", diff --git a/app/javascript/mastodon/main.tsx b/app/javascript/mastodon/main.tsx index 456cc21c31..f89baf66cd 100644 --- a/app/javascript/mastodon/main.tsx +++ b/app/javascript/mastodon/main.tsx @@ -9,11 +9,8 @@ import { me, reduceMotion } from 'mastodon/initial_state'; import ready from 'mastodon/ready'; import { store } from 'mastodon/store'; -import { - isProduction, - isDevelopment, - isModernEmojiEnabled, -} from './utils/environment'; +import { initializeEmoji } from './features/emoji'; +import { isProduction, isDevelopment } from './utils/environment'; function main() { perf.start('main()'); @@ -33,10 +30,7 @@ function main() { }); } - if (isModernEmojiEnabled()) { - const { initializeEmoji } = await import('@/mastodon/features/emoji'); - initializeEmoji(); - } + initializeEmoji(); const root = createRoot(mountNode); root.render(); diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 3b0c41be81..8fbc0cdf41 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -8,11 +8,10 @@ import type { ApiAccountRoleJSON, ApiAccountJSON, } from 'mastodon/api_types/accounts'; -import emojify from 'mastodon/features/emoji/emoji'; import { unescapeHTML } from 'mastodon/utils/html'; -import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji'; -import type { CustomEmoji, EmojiMap } from './custom_emoji'; +import { CustomEmojiFactory } from './custom_emoji'; +import type { CustomEmoji } from './custom_emoji'; // AccountField interface AccountFieldShape extends Required { @@ -102,17 +101,11 @@ export const accountDefaultValues: AccountShape = { const AccountFactory = ImmutableRecord(accountDefaultValues); -function createAccountField( - jsonField: ApiAccountFieldJSON, - emojiMap: EmojiMap, -) { +function createAccountField(jsonField: ApiAccountFieldJSON) { return AccountFieldFactory({ ...jsonField, - name_emojified: emojify( - escapeTextContentForBrowser(jsonField.name), - emojiMap, - ), - value_emojified: emojify(jsonField.value, emojiMap), + name_emojified: escapeTextContentForBrowser(jsonField.name), + value_emojified: jsonField.value, value_plain: unescapeHTML(jsonField.value), }); } @@ -120,8 +113,6 @@ function createAccountField( export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { const { moved, ...accountJSON } = serverJSON; - const emojiMap = makeEmojiMap(accountJSON.emojis); - const displayName = accountJSON.display_name.trim().length === 0 ? accountJSON.username @@ -134,7 +125,7 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { ...accountJSON, moved: moved?.id, fields: ImmutableList( - serverJSON.fields.map((field) => createAccountField(field, emojiMap)), + serverJSON.fields.map((field) => createAccountField(field)), ), emojis: ImmutableList( serverJSON.emojis.map((emoji) => CustomEmojiFactory(emoji)), @@ -142,11 +133,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) { roles: ImmutableList( serverJSON.roles?.map((role) => AccountRoleFactory(role)), ), - display_name_html: emojify( - escapeTextContentForBrowser(displayName), - emojiMap, - ), - note_emojified: emojify(accountNote, emojiMap), + display_name_html: escapeTextContentForBrowser(displayName), + note_emojified: accountNote, note_plain: unescapeHTML(accountNote), url: accountJSON.url?.startsWith('http://') || diff --git a/app/javascript/mastodon/models/poll.ts b/app/javascript/mastodon/models/poll.ts index 6f5655680d..46cbb1111d 100644 --- a/app/javascript/mastodon/models/poll.ts +++ b/app/javascript/mastodon/models/poll.ts @@ -1,10 +1,9 @@ import escapeTextContentForBrowser from 'escape-html'; import type { ApiPollJSON, ApiPollOptionJSON } from 'mastodon/api_types/polls'; -import emojify from 'mastodon/features/emoji/emoji'; -import { CustomEmojiFactory, makeEmojiMap } from './custom_emoji'; -import type { CustomEmoji, EmojiMap } from './custom_emoji'; +import { CustomEmojiFactory } from './custom_emoji'; +import type { CustomEmoji } from './custom_emoji'; interface PollOptionTranslation { title: string; @@ -17,16 +16,12 @@ export interface PollOption extends ApiPollOptionJSON { translation: PollOptionTranslation | null; } -export function createPollOptionTranslationFromServerJSON( - translation: { title: string }, - emojiMap: EmojiMap, -) { +export function createPollOptionTranslationFromServerJSON(translation: { + title: string; +}) { return { ...translation, - titleHtml: emojify( - escapeTextContentForBrowser(translation.title), - emojiMap, - ), + titleHtml: escapeTextContentForBrowser(translation.title), } as PollOptionTranslation; } @@ -50,8 +45,6 @@ export function createPollFromServerJSON( serverJSON: ApiPollJSON, previousPoll?: Poll, ) { - const emojiMap = makeEmojiMap(serverJSON.emojis); - return { ...pollDefaultValues, ...serverJSON, @@ -60,20 +53,15 @@ export function createPollFromServerJSON( const option = { ...optionJSON, voted: serverJSON.own_votes?.includes(index) || false, - titleHtml: emojify( - escapeTextContentForBrowser(optionJSON.title), - emojiMap, - ), + titleHtml: escapeTextContentForBrowser(optionJSON.title), } as PollOption; const prevOption = previousPoll?.options[index]; if (prevOption?.translation && prevOption.title === option.title) { const { translation } = prevOption; - option.translation = createPollOptionTranslationFromServerJSON( - translation, - emojiMap, - ); + option.translation = + createPollOptionTranslationFromServerJSON(translation); } return option; diff --git a/app/javascript/mastodon/polyfills/index.ts b/app/javascript/mastodon/polyfills/index.ts index 1abfe0a935..00da2042ed 100644 --- a/app/javascript/mastodon/polyfills/index.ts +++ b/app/javascript/mastodon/polyfills/index.ts @@ -2,9 +2,6 @@ // If there are no polyfills, then this is just Promise.resolve() which means // it will execute in the same tick of the event loop (i.e. near-instant). -// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension -import 'vite/modulepreload-polyfill'; - import { loadIntlPolyfills } from './intl'; function importExtraPolyfills() { @@ -17,6 +14,7 @@ export function loadPolyfills() { const needsExtraPolyfills = !window.requestIdleCallback; return Promise.all([ + loadVitePreloadPolyfill(), loadIntlPolyfills(), // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types needsExtraPolyfills ? importExtraPolyfills() : Promise.resolve(), @@ -31,5 +29,13 @@ async function loadEmojiPolyfills() { } } +// Loads Vite's module preload polyfill for older browsers, but not in a Worker context. +function loadVitePreloadPolyfill() { + if (typeof document === 'undefined') return; + // @ts-expect-error -- This is a virtual module provided by Vite. + // eslint-disable-next-line import/extensions + return import('vite/modulepreload-polyfill'); +} + // Null unless polyfill is needed. export let emojiRegexPolyfill: RegExp | null = null; diff --git a/app/javascript/mastodon/reducers/polls.ts b/app/javascript/mastodon/reducers/polls.ts index aadf6741c1..ac0917bd20 100644 --- a/app/javascript/mastodon/reducers/polls.ts +++ b/app/javascript/mastodon/reducers/polls.ts @@ -1,7 +1,6 @@ import type { Reducer } from '@reduxjs/toolkit'; import { importPolls } from 'mastodon/actions/importer/polls'; -import { makeEmojiMap } from 'mastodon/models/custom_emoji'; import { createPollOptionTranslationFromServerJSON } from 'mastodon/models/poll'; import type { Poll } from 'mastodon/models/poll'; @@ -20,16 +19,11 @@ const statusTranslateSuccess = (state: PollsState, pollTranslation?: Poll) => { if (!poll) return; - const emojiMap = makeEmojiMap(poll.emojis); - pollTranslation.options.forEach((item, index) => { const option = poll.options[index]; if (!option) return; - option.translation = createPollOptionTranslationFromServerJSON( - item, - emojiMap, - ); + option.translation = createPollOptionTranslationFromServerJSON(item); }); }; diff --git a/app/javascript/mastodon/stream.js b/app/javascript/mastodon/stream.js index 59b2fd7582..27fbc25ba5 100644 --- a/app/javascript/mastodon/stream.js +++ b/app/javascript/mastodon/stream.js @@ -138,10 +138,15 @@ const channelNameWithInlineParams = (channelName, params) => { return `${channelName}&${Object.keys(params).map(key => `${key}=${params[key]}`).join('&')}`; }; +/** + * @typedef {import('mastodon/store').AppDispatch} Dispatch + * @typedef {import('mastodon/store').GetState} GetState + */ + /** * @param {string} channelName * @param {Object.} params - * @param {function(Function, Function): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks + * @param {function(Dispatch, GetState): { onConnect: (function(): void), onReceive: (function(StreamEvent): void), onDisconnect: (function(): void) }} callbacks * @returns {function(): void} */ // @ts-expect-error @@ -229,7 +234,7 @@ const handleEventSourceMessage = (e, received) => { * @param {string} streamingAPIBaseURL * @param {string} accessToken * @param {string} channelName - * @param {{ connected: Function, received: function(StreamEvent): void, disconnected: Function, reconnected: Function }} callbacks + * @param {{ connected: function(): void, received: function(StreamEvent): void, disconnected: function(): void, reconnected: function(): void }} callbacks * @returns {WebSocketClient | EventSource} */ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { connected, received, disconnected, reconnected }) => { @@ -242,12 +247,9 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne // @ts-expect-error const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken); - // @ts-expect-error ws.onopen = connected; ws.onmessage = e => received(JSON.parse(e.data)); - // @ts-expect-error ws.onclose = disconnected; - // @ts-expect-error ws.onreconnect = reconnected; return ws; diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index c2b6b1cf86..95075454f2 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,16 +12,8 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures'; +export type Features = 'fasp' | 'http_message_signatures'; export function isFeatureEnabled(feature: Features) { return initialState?.features.includes(feature) ?? false; } - -export function isModernEmojiEnabled() { - try { - return isFeatureEnabled('modern_emojis'); - } catch { - return false; - } -} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 1b851803c4..0ef95b0146 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4031,6 +4031,7 @@ a.account__display-name { background: lighten($ui-highlight-color, 5%); } +.follow_requests-unlocked_explanation, .switch-to-advanced { color: $light-text-color; background-color: $ui-base-color; @@ -4041,7 +4042,7 @@ a.account__display-name { font-size: 13px; line-height: 18px; - .switch-to-advanced__toggle { + a { color: $ui-button-tertiary-color; font-weight: bold; } @@ -5223,8 +5224,7 @@ a.status-card { } } -.empty-column-indicator, -.follow_requests-unlocked_explanation { +.empty-column-indicator { color: $dark-text-color; text-align: center; padding: 20px; @@ -5263,10 +5263,8 @@ a.status-card { } .follow_requests-unlocked_explanation { - background: var(--surface-background-color); - border-bottom: 1px solid var(--background-border-color); - contain: initial; - flex-grow: 0; + margin: 16px; + margin-bottom: 0; } .error-column { diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 01ef33d061..0def7ab50f 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true class ActivityPub::Activity::Create < ActivityPub::Activity - include FormattingHelper - def perform @account.schedule_refresh_if_stale! @@ -99,9 +97,9 @@ class ActivityPub::Activity::Create < ActivityPub::Activity uri: @status_parser.uri, url: @status_parser.url || @status_parser.uri, account: @account, - text: converted_object_type? ? converted_text : (@status_parser.text || ''), + text: @status_parser.processed_text, language: @status_parser.language, - spoiler_text: converted_object_type? ? '' : (@status_parser.spoiler_text || ''), + spoiler_text: @status_parser.processed_spoiler_text, created_at: @status_parser.created_at, edited_at: @status_parser.edited_at && @status_parser.edited_at != @status_parser.created_at ? @status_parser.edited_at : nil, override_timestamps: @options[:override_timestamps], @@ -405,18 +403,6 @@ class ActivityPub::Activity::Create < ActivityPub::Activity value_or_id(@object['inReplyTo']) end - def converted_text - [formatted_title, @status_parser.spoiler_text.presence, formatted_url].compact.join("\n\n") - end - - def formatted_title - "

#{@status_parser.title}

" if @status_parser.title.present? - end - - def formatted_url - linkify(@status_parser.url || @status_parser.uri) - end - def unsupported_media_type?(mime_type) mime_type.present? && !MediaAttachment.supported_mime_types.include?(mime_type) end diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index 15025ca5e7..f158626dbb 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -8,10 +8,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity if equals_or_includes_any?(@object['type'], %w(Application Group Organization Person Service)) update_account - elsif equals_or_includes_any?(@object['type'], %w(Note Question)) + elsif supported_object_type? || converted_object_type? update_status - elsif converted_object_type? - Status.find_by(uri: object_uri, account_id: @account.id) end end diff --git a/app/lib/activitypub/parser/status_parser.rb b/app/lib/activitypub/parser/status_parser.rb index cbbea73056..9e2e208018 100644 --- a/app/lib/activitypub/parser/status_parser.rb +++ b/app/lib/activitypub/parser/status_parser.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ActivityPub::Parser::StatusParser + include FormattingHelper include JsonLdHelper NORMALIZED_LOCALE_NAMES = LanguagesHelper::SUPPORTED_LOCALES.keys.index_by(&:downcase).freeze @@ -44,6 +45,16 @@ class ActivityPub::Parser::StatusParser end end + def processed_text + return text || '' unless converted_object_type? + + [ + title.presence && "

#{title}

", + spoiler_text.presence, + linkify(url || uri), + ].compact.join("\n\n") + end + def spoiler_text if @object['summary'].present? @object['summary'] @@ -52,6 +63,12 @@ class ActivityPub::Parser::StatusParser end end + def processed_spoiler_text + return '' if converted_object_type? + + spoiler_text || '' + end + def title if @object['name'].present? @object['name'] @@ -147,6 +164,10 @@ class ActivityPub::Parser::StatusParser as_array(@object['quoteAuthorization']).first end + def converted_object_type? + equals_or_includes_any?(@object['type'], ActivityPub::Activity::CONVERTED_TYPES) + end + private def quote_subpolicy(subpolicy) diff --git a/app/lib/extractor.rb b/app/lib/extractor.rb index 7e647a7587..206d989bf3 100644 --- a/app/lib/extractor.rb +++ b/app/lib/extractor.rb @@ -54,7 +54,7 @@ module Extractor end def extract_hashtags_with_indices(text, _options = {}) - return [] unless text&.index('#') + return [] unless text&.index(/[##]/) possible_entries = [] diff --git a/app/lib/signed_request.rb b/app/lib/signed_request.rb index ca86460e67..d8887d9596 100644 --- a/app/lib/signed_request.rb +++ b/app/lib/signed_request.rb @@ -238,7 +238,7 @@ class SignedRequest def initialize(request) @signature = - if Mastodon::Feature.http_message_signatures_enabled? && request.headers['signature-input'].present? + if request.headers['signature-input'].present? HttpMessageSignature.new(request) else HttpSignature.new(request) diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb index 5ba706c4c0..b6a5e37056 100644 --- a/app/lib/status_cache_hydrator.rb +++ b/app/lib/status_cache_hydrator.rb @@ -61,6 +61,7 @@ class StatusCacheHydrator payload[:filtered] = payload[:reblog][:filtered] payload[:favourited] = payload[:reblog][:favourited] payload[:reblogged] = payload[:reblog][:reblogged] + payload[:quote_approval] = payload[:reblog][:quote_approval] end end diff --git a/app/models/concerns/status/fetch_replies_concern.rb b/app/models/concerns/status/fetch_replies_concern.rb index 7ab4648174..6d65fe41cb 100644 --- a/app/models/concerns/status/fetch_replies_concern.rb +++ b/app/models/concerns/status/fetch_replies_concern.rb @@ -4,8 +4,10 @@ module Status::FetchRepliesConcern extend ActiveSupport::Concern # debounce fetching all replies to minimize DoS - FETCH_REPLIES_COOLDOWN_MINUTES = (ENV['FETCH_REPLIES_COOLDOWN_MINUTES'] || 15).to_i.minutes - FETCH_REPLIES_INITIAL_WAIT_MINUTES = (ENV['FETCH_REPLIES_INITIAL_WAIT_MINUTES'] || 5).to_i.minutes + # Period to wait between fetching replies + FETCH_REPLIES_COOLDOWN_MINUTES = 15.minutes + # Period to wait after a post is first created before fetching its replies + FETCH_REPLIES_INITIAL_WAIT_MINUTES = 5.minutes included do scope :created_recently, -> { where(created_at: FETCH_REPLIES_INITIAL_WAIT_MINUTES.ago..) } diff --git a/app/models/tag.rb b/app/models/tag.rb index dff1011112..f9eb6bfd33 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -41,7 +41,7 @@ class Tag < ApplicationRecord HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)' HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}".freeze - HASHTAG_RE = %r{(? @status.edited_at) @@ -170,8 +169,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def update_immediate_attributes! - @status.text = @status_parser.text || '' - @status.spoiler_text = @status_parser.spoiler_text || '' + @status.text = @status_parser.processed_text + @status.spoiler_text = @status_parser.processed_spoiler_text @status.sensitive = @account.sensitized? || @status_parser.sensitive || false @status.language = @status_parser.language @@ -351,7 +350,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService end def expected_type? - equals_or_includes_any?(@json['type'], %w(Note Question)) + equals_or_includes_any?(@json['type'], ActivityPub::Activity::SUPPORTED_TYPES) || equals_or_includes_any?(@json['type'], ActivityPub::Activity::CONVERTED_TYPES) end def record_previous_edit! diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index c43333c0ea..553c58f973 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -21,16 +21,15 @@ selected: current_user.time_zone || Time.zone.tzinfo.name, wrapper: :with_label - - if Mastodon::Feature.modern_emojis_enabled? - .fields-group - = f.simple_fields_for :settings, current_user.settings do |ff| - = ff.input :'web.emoji_style', - collection: %w(auto twemoji native), - include_blank: false, - hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), - label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), - label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) }, - wrapper: :with_label + .fields-group + = f.simple_fields_for :settings, current_user.settings do |ff| + = ff.input :'web.emoji_style', + collection: %w(auto twemoji native), + include_blank: false, + hint: I18n.t('simple_form.hints.defaults.setting_emoji_style'), + label: I18n.t('simple_form.labels.defaults.setting_emoji_style'), + label_method: ->(emoji_style) { I18n.t("emoji_styles.#{emoji_style}", default: emoji_style) }, + wrapper: :with_label - unless I18n.locale == :en .flash-message.translation-prompt diff --git a/app/workers/activitypub/fetch_all_replies_worker.rb b/app/workers/activitypub/fetch_all_replies_worker.rb index 128bfe7e8a..2e91a3e95b 100644 --- a/app/workers/activitypub/fetch_all_replies_worker.rb +++ b/app/workers/activitypub/fetch_all_replies_worker.rb @@ -11,9 +11,10 @@ class ActivityPub::FetchAllRepliesWorker sidekiq_options queue: 'pull', retry: 3 - # Global max replies to fetch per request (all replies, recursively) - MAX_REPLIES = (ENV['FETCH_REPLIES_MAX_GLOBAL'] || 1000).to_i - MAX_PAGES = (ENV['FETCH_REPLIES_MAX_PAGES'] || 500).to_i + # Max number of replies to fetch - total, recursively through a whole reply tree + MAX_REPLIES = 1000 + # Max number of replies Collection pages to fetch - total + MAX_PAGES = 500 def perform(root_status_id, options = {}) @batch = WorkerBatch.new(options['batch_id']) diff --git a/config/locales/be.yml b/config/locales/be.yml index b265585e4a..2a9a61c347 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -867,7 +867,7 @@ be: title: Перадвызначана выключыць карыстальнікаў з індэксацыі пашуковымі рухавікамі discovery: follow_recommendations: Выконвайце рэкамендацыі - preamble: Паказ цікавага кантэнту карысны ў прывабліванні новых карыстальнікаў, якія могуць нікога не ведаць у Mastodon. Кантралюйце, як розныя функцыі вынаходства працуюць на Вашым серверы. + preamble: Паказ цікавага кантэнту карысны ў прывабліванні новых карыстальнікаў, якія могуць нікога не ведаць у Mastodon. Кантралюйце, як розныя функцыі выяўлення працуюць на Вашым серверы. privacy: Прыватнасць profile_directory: Дырэкторыя профіляў public_timelines: Публічная паслядоўнасць публікацый @@ -883,6 +883,11 @@ be: authenticated: Толькі аўтэнтыфікаваныя карыстальнікі disabled: Запатрабаваць адмысловую ролю карыстальніка public: Усе + landing_page: + values: + about: Падрабязна + local_feed: Тутэйшая стужка + trends: Трэнды registrations: moderation_recommandation: Пераканайцеся, што ў вас ёсць адэкватная і аператыўная каманда мадэратараў, перш чым адчыняць рэгістрацыю для ўсіх жадаючых! preamble: Кантралюйце, хто можа ствараць уліковы запіс на вашым серверы. diff --git a/config/locales/bg.yml b/config/locales/bg.yml index 60a86c36a7..251eef9cb9 100644 --- a/config/locales/bg.yml +++ b/config/locales/bg.yml @@ -835,6 +835,14 @@ bg: all: До всеки disabled: До никого users: До влезнали локални потребители + feed_access: + modes: + authenticated: Само удостоверени потребители + disabled: Изисква особена потребителска роля + public: Всеки + landing_page: + values: + trends: Пламенности registrations: moderation_recommandation: Уверете се, че имате адекватен и реактивен модераторски екип преди да отворите регистриранията за всеки! preamble: Управлява кой може да създава акаунт на сървъра ви. @@ -888,6 +896,7 @@ bg: no_status_selected: Няма промяна, тъй като няма избрани публикации open: Отваряне на публикация original_status: Първообразна публикация + quotes: Цитати reblogs: Блогване пак replied_to_html: Отговорено до %{acct_link} status_changed: Публикацията променена @@ -895,6 +904,7 @@ bg: title: Публикации на акаунт - @%{name} trending: Изгряващи view_publicly: Преглед като публично + view_quoted_post: Преглед на цитираната публикация visibility: Видимост with_media: С мултимедия strikes: @@ -1165,7 +1175,10 @@ bg: hint_html: Ако желаете да се преместите от друг акаунт към този, тук можете да създадете псевдоним, което се изисква преди да можете да пристъпите към преместване на последователите си от стария акаунт към този. Това действие е безопасно и възстановимо. Миграцията към новия акаунт се инициира от стария акаунт. remove: Разкачвне на псевдонима appearance: + advanced_settings: Разширени настройки animations_and_accessibility: Анимация и достъпност + boosting_preferences: Настройки за подсилване + boosting_preferences_info_html: "Съвет: Без значение от настройките, Shift + Щрак върху иконата %{icon} Подсилване веднага ще подсили." discovery: Откриване localization: body: Mastodon е преведено от доброволци. @@ -1567,6 +1580,13 @@ bg: expires_at: Изтича на uses: Използвания title: Поканете хора + link_preview: + author_html: От %{name} + potentially_sensitive_content: + action: Щракване за показване + confirm_visit: Наистина ли искате да отворите тази връзка? + hide_button: Скриване + label: Възможно деликатно съдържание lists: errors: limit: Достигнахте максималния брой списъци @@ -1719,6 +1739,9 @@ bg: self_vote: Не може да гласувате в свои анкети too_few_options: трябва да има повече от един елемент too_many_options: не може да съдържа повече от %{max} елемента + vote: Гласувам + posting_defaults: + explanation: Тези настройки ще се употребяват като стандартни, когато създавате нови публикации, но може да ги редактирате за всяка публикация в редактора. preferences: other: Друго posting_defaults: По подразбиране за публикации @@ -1874,6 +1897,9 @@ bg: other: "%{count} видеозаписа" boosted_from_html: Раздуто от %{acct_link} content_warning: 'Предупреждение за съдържание: %{warning}' + content_warnings: + hide: Скриване на публ. + show: Показване на още default_language: Същият като езика на интерфейса disallowed_hashtags: one: 'съдържа непозволен хаштаг: %{tags}' @@ -1888,9 +1914,22 @@ bg: limit: Вече сте закачили максималния брой публикации ownership: Публикация на някого другиго не може да бъде закачена reblog: Раздуване не може да бъде закачано + quote_error: + not_available: Неналична публикация + pending_approval: Публикацията чака одобрение + revoked: Премахната публикация от автора + quote_policies: + followers: Само последователи + nobody: Само аз + public: Някой + quote_post_author: Цитирах публикация от %{acct} title: "%{name}: „%{quote}“" visibilities: + direct: Частно споменаване + private: Само последователи public: Публично + public_long: Всеки във и извън Mastodon + unlisted: Тиха публика statuses_cleanup: enabled: Автоматично изтриване на стари публикации enabled_hint: От само себе си трие публикациите ви, щом достигнат указания възрастов праг, освен ако не съвпаднат с някое от изключенията долу diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 2c8f4c8615..9c69ab8449 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -872,7 +872,7 @@ cs: profile_directory: Adresář profilů public_timelines: Veřejné časové osy publish_statistics: Zveřejnit statistiku - title: Objevujte + title: Objevování trends: Trendy domain_blocks: all: Všem @@ -883,6 +883,11 @@ cs: authenticated: Pouze autentifikovaní uživatelé disabled: Vyžadovat specifickou uživatelskou roli public: Všichni + landing_page: + values: + about: O službě + local_feed: Místní kanál + trends: Trendy registrations: moderation_recommandation: Před otevřením registrací všem se ujistěte, že máte vhodný a reaktivní moderační tým! preamble: Mějte pod kontrolou, kdo může vytvořit účet na vašem serveru. diff --git a/config/locales/da.yml b/config/locales/da.yml index 2114169d14..c6a30afaae 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -855,6 +855,11 @@ da: authenticated: Kun godkendte brugere disabled: Kræv specifik brugerrolle public: Alle + landing_page: + values: + about: Om + local_feed: Lokalt feed + trends: Trends registrations: moderation_recommandation: Sørg for, at der er et tilstrækkeligt og reaktivt moderationsteam, før registrering åbnes for alle! preamble: Styr, hvem der kan oprette en konto på serveren. diff --git a/config/locales/de.yml b/config/locales/de.yml index 450bdb4361..e4a579575f 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -852,6 +852,9 @@ de: modes: authenticated: Nur authentifizierte Nutzer*innen public: Alle + landing_page: + values: + about: Über registrations: moderation_recommandation: Bitte vergewissere dich, dass du ein geeignetes und reaktionsschnelles Moderationsteam hast, bevor du die Registrierungen uneingeschränkt zulässt! preamble: Lege fest, wer auf deinem Server ein Konto erstellen darf. diff --git a/config/locales/devise.bg.yml b/config/locales/devise.bg.yml index 5cb41fa616..612658b6bd 100644 --- a/config/locales/devise.bg.yml +++ b/config/locales/devise.bg.yml @@ -7,6 +7,7 @@ bg: send_paranoid_instructions: Ако вашият имейл адрес съществува в нашата база данни, ще получите имейл с указания как да потвърдите имейл адреса си след няколко минути. Проверете спам папката си, ако не сте получили такъв имейл. failure: already_authenticated: Вече сте влезли. + closed_registrations: Вашият опит за регистриране е блокиран заради мрежова политика. Ако вярвате, че е грешка, то свържете се с %{email}. inactive: Акаунтът ви още не е задействан. invalid: Невалиден %{authentication_keys} или парола. last_attempt: Разполагате с още един опит преди акаунтът ви да се заключи. diff --git a/config/locales/devise.pt-BR.yml b/config/locales/devise.pt-BR.yml index aa1190dfd0..89a3800cf4 100644 --- a/config/locales/devise.pt-BR.yml +++ b/config/locales/devise.pt-BR.yml @@ -7,6 +7,7 @@ pt-BR: send_paranoid_instructions: Se o seu endereço de e-mail já existir em nosso banco de dados, você receberá um e-mail com instruções para confirmá-lo dentro de alguns minutos. Verifique sua caixa de spam caso ainda não o tenha recebido. failure: already_authenticated: Você entrou na sua conta. + closed_registrations: Sua tentativa de registro foi bloqueada devido a uma política de rede. Se você acredita que isso é um erro, entre em contato com %{email}. inactive: Sua conta não foi confirmada ainda. invalid: "%{authentication_keys} ou senha inválida." last_attempt: Você tem mais uma tentativa antes de sua conta ser bloqueada. diff --git a/config/locales/el.yml b/config/locales/el.yml index aa0aec3a81..0f2339b1ca 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -855,6 +855,11 @@ el: authenticated: Πιστοποιημένοι χρήστες μόνο disabled: Να απαιτείται συγκεκριμένος ρόλος χρήστη public: Όλοι + landing_page: + values: + about: Σχετικά + local_feed: Τοπική ροή + trends: Τάσεις registrations: moderation_recommandation: Παρακαλώ βεβαιώσου ότι έχεις μια επαρκής και ενεργή ομάδα συντονισμού πριν ανοίξεις τις εγγραφές για όλους! preamble: Έλεγξε ποιος μπορεί να δημιουργήσει ένα λογαριασμό στον διακομιστή σας. diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index bb82fe39e1..1297355469 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -855,6 +855,11 @@ es-AR: authenticated: Solo usuarios autenticados disabled: Requerir un rol de específico de usuario public: Todos + landing_page: + values: + about: Acerca de + local_feed: Cronología local + trends: Tendencias registrations: moderation_recommandation: Por favor, ¡asegurate de tener un equipo de moderación adecuado y reactivo antes de abrir los registros a todos! preamble: Controlá quién puede crear una cuenta en tu servidor. diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index f5c52260cc..ecfc06836e 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -855,6 +855,11 @@ es-MX: authenticated: Solo usuarios autenticados disabled: Requerir un rol de usuario específico public: Todos + landing_page: + values: + about: Acerca de + local_feed: Cronología local + trends: Tendencias registrations: moderation_recommandation: "¡Por favor, asegúrate de contar con un equipo de moderación adecuado y activo antes de abrir el registro al público!" preamble: Controla quién puede crear una cuenta en tu servidor. diff --git a/config/locales/es.yml b/config/locales/es.yml index 2264696795..da0d9ac34b 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -855,6 +855,11 @@ es: authenticated: Solo usuarios autenticados disabled: Requerir un rol de usuario específico public: Todos + landing_page: + values: + about: Acerca de + local_feed: Cronología local + trends: Tendencias registrations: moderation_recommandation: Por favor, ¡asegúrate de tener un equipo de moderación adecuado y reactivo antes de abrir los registros a todo el mundo! preamble: Controla quién puede crear una cuenta en tu servidor. diff --git a/config/locales/et.yml b/config/locales/et.yml index 63adff4c7c..8f56b20b1a 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -837,6 +837,7 @@ et: title: Otsimootorite indeksitesse kasutajaid vaikimisi ei lisata discovery: follow_recommendations: Jälgi soovitusi + preamble: Huvitava sisu esiletoomine on oluline uute kasutajate kaasamisel, kes ei pruugi Mastodonist kedagi tunda. Kontrolli, kuidas erinevad avastamisfunktsioonid serveris töötavad. privacy: Privaatsus profile_directory: Kasutajate kataloog public_timelines: Avalikud ajajooned @@ -850,7 +851,11 @@ et: feed_access: modes: authenticated: Vaid autenditud kasutajad + disabled: Eelda konkreetse kasutajarolli olemasolu public: Kõik + landing_page: + values: + trends: Trendid registrations: moderation_recommandation: Enne kõigi jaoks registreerimise avamist veendu, et oleks olemas adekvaatne ja reageerimisvalmis modereerijaskond! preamble: Kes saab serveril konto luua. diff --git a/config/locales/fo.yml b/config/locales/fo.yml index db217841e6..3938235eee 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -855,6 +855,11 @@ fo: authenticated: Einans váttaðir brúkarar disabled: Krev serstakan brúkaraleiklut public: Øll + landing_page: + values: + about: Um + local_feed: Lokal rás + trends: Rák registrations: moderation_recommandation: Vinarliga tryggja tær, at tú hevur eitt nøktandi og klárt umsjónartoymi, áðreen tú letur upp fyri skrásetingum frá øllum! preamble: Stýr, hvør kann stovna eina kontu á tínum ambætara. diff --git a/config/locales/ga.yml b/config/locales/ga.yml index fa91f853ea..cb27ec1f9e 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -897,6 +897,11 @@ ga: authenticated: Úsáideoirí fíordheimhnithe amháin disabled: Éiligh ról úsáideora sonrach public: Gach duine + landing_page: + values: + about: Maidir + local_feed: Fotha áitiúil + trends: Treochtaí registrations: moderation_recommandation: Cinntigh le do thoil go bhfuil foireann mhodhnóireachta imoibríoch leordhóthanach agat sula n-osclaíonn tú clárúcháin do gach duine! preamble: Rialú cé atá in ann cuntas a chruthú ar do fhreastalaí. diff --git a/config/locales/he.yml b/config/locales/he.yml index bac4c44bdc..eebc9000da 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -883,6 +883,11 @@ he: authenticated: משתמשים מאומתים בלבד disabled: נדרש תפקיד משתמש מסוים public: כולם + landing_page: + values: + about: אודות + local_feed: פיד מקומי + trends: נושאים חמים registrations: moderation_recommandation: יש לוודא שלאתר יש צוות מנחות ומנחי שיחה מספק ושירותי בטרם תבחרו לפתוח הרשמה לכולם! preamble: שליטה בהרשאות יצירת חשבון בשרת שלך. diff --git a/config/locales/ia.yml b/config/locales/ia.yml index 6918a93cc4..b08b01e0ef 100644 --- a/config/locales/ia.yml +++ b/config/locales/ia.yml @@ -320,7 +320,7 @@ ia: edit: title: Modificar annuncio empty: Necun annuncios trovate. - live: In directo + live: In vivo new: create: Crear annuncio title: Nove annuncio @@ -796,6 +796,8 @@ ia: view_dashboard_description: Permitte que usatores accede al tabuliero de instrumentos e a varie statisticas view_devops: DevOps view_devops_description: Permitte que usatores accede al tabulieros de instrumentos de Sidekiq e pgHero + view_feeds: Vider canales thematic e in vivo + view_feeds_description: Permitte que usatores acceder al canales thematic e in vivo independentemente del configuration del servitor title: Rolos rules: add_new: Adder regula @@ -837,6 +839,7 @@ ia: title: Excluder le usatores del indexation del motores de recerca per predefinition discovery: follow_recommendations: Recommendationes de contos a sequer + preamble: Presentar contento interessante es essential pro attraher e retener nove usatores qui pote non cognoscer alcun persona sur Mastodon. Controla como varie optiones de discoperta functiona sur tu servitor. privacy: Confidentialitate profile_directory: Directorio de profilos public_timelines: Chronologias public @@ -850,7 +853,13 @@ ia: feed_access: modes: authenticated: Solmente usatores authenticate + disabled: Requirer un rolo de usator specific public: Omnes + landing_page: + values: + about: A proposito + local_feed: Canal local + trends: Tendentias registrations: moderation_recommandation: Per favor assecura te de haber un equipa de moderation adequate e reactive ante de aperir le inscription a omnes! preamble: Controla qui pote crear un conto sur tu servitor. diff --git a/config/locales/is.yml b/config/locales/is.yml index 4cb9da51a4..104bd63894 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -857,6 +857,11 @@ is: authenticated: Einungis auðkenndir notendur disabled: Krefjast sérstaks hlutverks notanda public: Allir + landing_page: + values: + about: Um hugbúnaðinn + local_feed: Staðbundið streymi + trends: Vinsælt registrations: moderation_recommandation: Tryggðu að þú hafir hæft og aðgengilegt umsjónarteymi til taks áður en þú opnar á skráningar fyrir alla! preamble: Stýrðu því hverjir geta útbúið notandaaðgang á netþjóninum þínum. diff --git a/config/locales/lad.yml b/config/locales/lad.yml index 5c982eff3d..c15f513ce2 100644 --- a/config/locales/lad.yml +++ b/config/locales/lad.yml @@ -693,6 +693,7 @@ lad: delete_data_html: Efasa el profil i kontenido de @%{acct} en 30 dias si no sea desuspendido en akel tiempo preview_preamble_html: "@%{acct} resivira una avertensya komo esta:" record_strike_html: Enrejistra un amonestamiento kontra @%{acct} para ke te ayude eskalar las violasyones de reglas de este kuento en el avenir + send_email_html: Embia un mesaj de avertensia a la posta elektronika de @%{acct} warning_placeholder: Adisionalas, opsionalas razones la aksyon de moderasyon. target_origin: Orijin del kuento raportado title: Raportos @@ -796,6 +797,7 @@ lad: title: Ekskluye utilizadores de la indeksasyon de los bushkadores komo preferensya predeterminada discovery: follow_recommendations: Rekomendasyones de kuentos + preamble: Ekspone kontenido enteresante a la superfisie es fundamental para inkorporar muevos utilizadores ke pueden no koneser a dinguno en Mastodon. Kontrola komo fonksionan varias opsiones de diskuvrimiento en tu sirvidor. privacy: Privasita profile_directory: Katalogo de profiles public_timelines: Linyas de tiempo publikas @@ -809,6 +811,10 @@ lad: feed_access: modes: public: Todos + landing_page: + values: + about: Sovre esto + trends: Trendes registrations: moderation_recommandation: Por favor, asigurate ke tyenes una taifa de moderasyon adekuada i reaktiva antes de avrir los enrejistramyentos a todos! preamble: Kontrola ken puede kriyar un kuento en tu sirvidor. @@ -846,6 +852,7 @@ lad: back_to_account: Retorna al kuento back_to_report: Retorna a la pajina del raporto batch: + add_to_report: 'Adjusta al raporto #%{id}' remove_from_report: Kita del raporto report: Raporto contents: Kontenidos @@ -860,10 +867,12 @@ lad: no_status_selected: No se troko dinguna publikasyon al no eskojer dinguna open: Avre publikasyon original_status: Publikasyon orijinala + quotes: Sitas reblogs: Repartajasyones status_changed: Publikasyon trokada trending: Trendes view_publicly: Ve puvlikamente + view_quoted_post: Ve puvlikasyon sitada visibility: Vizivilita with_media: Kon multimedia strikes: @@ -1094,7 +1103,9 @@ lad: hint_html: Si keres migrar de otro kuento a este, aki puedes kriyar un alias, kale proseder antes de ampesar a mover suivantes del kuento anterior a este. Esta aksion por si mezma es inofensiva i reversivle. La migrasyon del kuento se inisya dizde el kuento viejo. remove: Dezata alias appearance: + advanced_settings: Konfigurasyon avansada animations_and_accessibility: Animasyones i aksesivilita + boosting_preferences: Preferensias de repartajar discovery: Diskuvrimiento localization: body: Mastodon es trezladado por volontarios. @@ -1199,6 +1210,7 @@ lad: example_title: Teksto de enshemplo more_from_html: Mas de %{name} s_blog: Blog de %{name} + title: Atribusyon del otor challenge: confirm: Kontinua hint_html: "Konsejo: No retornaremos a demandarte por el kod durante la sigiente ora." @@ -1734,6 +1746,7 @@ lad: preferences: Preferensyas profile: Profil publiko relationships: Segidos i suivantes + severed_relationships: Relasyones kortadas statuses_cleanup: Efasasyon otomatika de publikasyones strikes: Amonestamientos de moderasyon two_factor_authentication: Autentifikasyon en dos pasos @@ -1741,6 +1754,8 @@ lad: severed_relationships: download: Abasha (%{count}) event_type: + account_suspension: Suspensyon de kuento (%{target_name}) + domain_block: Suspensyon de sirvidor (%{target_name}) user_domain_block: Blokates a %{target_name} lost_followers: Suivantes pedridos lost_follows: Segimyentos pedridos @@ -1776,10 +1791,15 @@ lad: limit: Ya tienes fiksado el numero maksimo de publikasyones ownership: La publikasyon de otra persona no puede fiksarse reblog: No se puede fixar una repartajasyon + quote_error: + not_available: Puvlikasyon no desponivle + pending_approval: Puvlikasyon esta asperando + revoked: Puvlikasyon kitada por el otor quote_policies: followers: Solo suivantes nobody: Solo yo public: Todos + quote_post_author: Sito una puvlikasyon de %{acct} title: '%{name}: "%{quote}"' visibilities: direct: Enmentadura privada @@ -1893,6 +1913,8 @@ lad: subject: Tu kuento fue aksedido dizde un muevo adreso IP title: Una mueva koneksyon kon tu kuento terms_of_service_changed: + sign_off: La taifa de %{domain} + subject: Aktualizasyones de muestros terminos de sirvisyo title: Aktualizasyon emportante warning: appeal: Embia una apelasyon diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 241a22f02f..9179337c6f 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -855,6 +855,11 @@ nl: authenticated: Alleen ingelogde gebruikers disabled: Specifieke gebruikersrol vereisen public: Iedereen + landing_page: + values: + about: Over + local_feed: Lokale tijdlijn + trends: Trends registrations: moderation_recommandation: Zorg ervoor dat je een adequaat en responsief moderatieteam hebt voordat je registraties voor iedereen openstelt! preamble: Toezicht houden op wie een account op deze server kan registreren. diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index 615432f170..44e6d18311 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -847,6 +847,16 @@ pt-BR: all: Para todos disabled: Para ninguém users: Para usuários locais logados + feed_access: + modes: + authenticated: Apenas usuários autenticados + disabled: Exige função específica de usuário + public: Todos + landing_page: + values: + about: Sobre + local_feed: Feed local + trends: Em alta registrations: moderation_recommandation: Por favor, certifique-se de ter uma equipe de moderação adequada e reativa antes de abrir as inscrições para todos! preamble: Controle quem pode criar uma conta no seu servidor. @@ -900,6 +910,7 @@ pt-BR: no_status_selected: Nenhuma publicação foi modificada porque nenhuma estava selecionada open: Publicação aberta original_status: Publicação original + quotes: Citações reblogs: Reblogs replied_to_html: Respondeu à %{acct_link} status_changed: Publicação alterada @@ -907,6 +918,7 @@ pt-BR: title: Publicações da conta - @%{name} trending: Em alta view_publicly: Ver publicamente + view_quoted_post: Visualizar citação publicada visibility: Visibilidade with_media: Com mídia strikes: @@ -1181,7 +1193,9 @@ pt-BR: hint_html: Se você quiser migrar de uma outra conta para esta, você pode criar um atalho aqui, o que é necessário antes que você possa migrar os seguidores da conta antiga para esta. Esta ação por si só é inofensiva e reversível. A migração da conta é iniciada pela conta antiga. remove: Desvincular alias appearance: + advanced_settings: Configurações avançadas animations_and_accessibility: Animações e acessibilidade + boosting_preferences: Adicionar preferências discovery: Descobrir localization: body: Mastodon é traduzido por voluntários. @@ -1583,6 +1597,13 @@ pt-BR: expires_at: Expira em uses: Usos title: Convidar pessoas + link_preview: + author_html: Por %{name} + potentially_sensitive_content: + action: Clique para mostrar + confirm_visit: Tem certeza que deseja abrir esse link? + hide_button: Ocultar + label: Conteúdo potencialmente sensível lists: errors: limit: Você atingiu o número máximo de listas @@ -1893,6 +1914,9 @@ pt-BR: other: "%{count} vídeos" boosted_from_html: Impulso de %{acct_link} content_warning: 'Aviso de conteúdo: %{warning}' + content_warnings: + hide: Ocultar publicação + show: Exibir mais default_language: Igual ao idioma da interface disallowed_hashtags: one: 'continha hashtag não permitida: %{tags}' @@ -1907,15 +1931,22 @@ pt-BR: limit: Você alcançou o número limite de publicações fixadas ownership: As publicações dos outros não podem ser fixadas reblog: Um impulso não pode ser fixado + quote_error: + not_available: Publicação indisponível + pending_approval: Publicação pendente + revoked: Publicação removida pelo autor quote_policies: followers: Apenas seguidores nobody: Apenas eu public: Qualquer um + quote_post_author: Publicação citada por %{acct} title: '%{name}: "%{quote}"' visibilities: direct: Citação privada + private: Apenas seguidores public: Público public_long: Qualquer um dentro ou fora do Mástodon + unlisted: Publicação silenciada unlisted_long: Oculto aos resultados de pesquisa em Mástodon statuses_cleanup: enabled: Excluir publicações antigas automaticamente diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index b2e802300b..24a21f886c 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -796,6 +796,8 @@ pt-PT: view_dashboard_description: Permite aos utilizadores acederem ao painel de controlo e a várias estatísticas view_devops: DevOps view_devops_description: Permite aos utilizadores aceder aos painéis de controlo do Sidekiq e pgHero + view_feeds: Ver cronologia em tempo real e de etiquetas + view_feeds_description: Permitir aos utilizadores aceder às cronologias em tempo real e de etiquetas independentemente das definições do servidor title: Funções rules: add_new: Adicionar regra @@ -851,7 +853,13 @@ pt-PT: feed_access: modes: authenticated: Apesar utilizadores autenticados + disabled: Requerer função de utilizador especifica public: Todos + landing_page: + values: + about: Sobre + local_feed: Cronologia local + trends: Tendências registrations: moderation_recommandation: Certifique-se de que dispõe de uma equipa de moderação adequada e reativa antes de abrir as inscrições a todos! preamble: Controle quem pode criar uma conta no seu servidor. diff --git a/config/locales/simple_form.be.yml b/config/locales/simple_form.be.yml index e2d90ad1b8..c097562a2a 100644 --- a/config/locales/simple_form.be.yml +++ b/config/locales/simple_form.be.yml @@ -93,6 +93,7 @@ be: content_cache_retention_period: Усе допісы з іншых сервераў (разам з пашырэннямі і адказамі) будуць выдалены праз паказаную колькасць дзён, незалежна ад таго, як лакальны карыстальнік узаемадзейнічаў з гэтымі допісамі. Гэта датычыцца і тых допісаў, якія лакальны карыстальнік пазначыў у закладкі або ўпадабанае. Прыватныя згадванні паміж карыстальнікамі з розных экзэмпляраў сервераў таксама будуць страчаны і іх нельга будзе аднавіць. Выкарыстанне гэтай налады прызначана для экзэмпляраў сервераў спецыяльнага прызначэння і парушае многія чаканні карыстальнікаў пры выкарыстанні ў агульных мэтах. custom_css: Вы можаце прымяняць карыстальніцкія стылі ў вэб-версіі Mastodon. favicon: WEBP, PNG, GIF ці JPG. Замяняе прадвызначаны favicon Mastodon на ўласны значок. + landing_page: Выбірае, якую старонку бачаць новыя наведвальнікі, калі прыходзяць на Ваш сервер. Калі выбераце "Трэнды", тады неабходна іх уключыць у наладах Выяўленне. Калі выбераце "Тутэйшая стужка", тады ў наладах Выяўленне ў налады "Доступ да жывых стужак з лакальнымі допісамі" мусіць стаяць варыянт "Усе". mascot: Замяняе ілюстрацыю ў пашыраным вэб-інтэрфейсе. media_cache_retention_period: Медыяфайлы з допісаў, зробленых карыстальнікамі з іншых сервераў, кэшыруюцца на вашым серверы. Пры станоўчым значэнні медыяфайлы будуць выдалены праз пазначаную колькасць дзён. Калі медыяданыя будуць запытаныя пасля выдалення, яны будуць спампаваныя зноў, калі зыходнае змесціва усё яшчэ даступнае. У сувязі з абмежаваннямі на частату абнаўлення картак перадпрагляду іншых сайтаў, рэкамендуецца ўсталяваць значэнне не менш за 14 дзён, інакш гэтыя карткі не будуць абнаўляцца па запыце раней за гэты тэрмін. min_age: Карыстальнікі будуць атрымліваць запыт на пацвярджэнне даты нараджэння падчас рэгістрацыі @@ -288,6 +289,7 @@ be: content_cache_retention_period: Перыяд захоўвання змесціва з іншых сервераў custom_css: CSS карыстальніка favicon: Значок сайта + landing_page: Старонка прыбыцця для новых наведвальнікаў local_live_feed_access: Доступ да жывых стужак з лакальнымі допісамі local_topic_feed_access: Доступ да хэштэгавых і спасылачных стужак з лакальнымі допісамі mascot: Уласны маскот(спадчына) diff --git a/config/locales/simple_form.bg.yml b/config/locales/simple_form.bg.yml index 281124a6b4..e14b05ed90 100644 --- a/config/locales/simple_form.bg.yml +++ b/config/locales/simple_form.bg.yml @@ -242,6 +242,7 @@ bg: setting_emoji_style: Стил на емоджито setting_expand_spoilers: Винаги разширяване на публикации, отбелязани с предупреждения за съдържание setting_hide_network: Скриване на социалния ви свързан граф + setting_quick_boosting: Включване на бързо подсилване setting_reduce_motion: Обездвижване на анимациите setting_system_font_ui: Употреба на стандартния шрифт на системата setting_system_scrollbars_ui: Употреба на системната подразбираща се лента за превъртане diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index d8b803fe1f..cd5a51901a 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -93,6 +93,7 @@ cs: content_cache_retention_period: Všechny příspěvky z jiných serverů (včetně boostů a odpovědí) budou po uplynutí stanoveného počtu dní smazány bez ohledu na interakci místního uživatele s těmito příspěvky. To se týká i příspěvků, které místní uživatel přidal do záložek nebo oblíbených. Soukromé zmínky mezi uživateli z různých instancí budou rovněž ztraceny a nebude možné je obnovit. Použití tohoto nastavení je určeno pro instance pro speciální účely a při implementaci pro obecné použití porušuje mnohá očekávání uživatelů. custom_css: Můžete použít vlastní styly ve verzi Mastodonu. favicon: WEBP, PNG, GIF nebo JPG. Nahradí výchozí favicon Mastodonu vlastní ikonou. + landing_page: Vybere stránku, kterou návštěvníci uvidí, když prvně přijdou na tvůj server. Pokud zvolíte "Trendy", je třeba povolit trendy v nastavení objevování. Pokud zvolíte "Místní kanál", je třeba v nastavení Objevování nastavit "Přístup k živým kanálům s lokálními příspěvky" na "Všichni". mascot: Přepíše ilustraci v pokročilém webovém rozhraní. media_cache_retention_period: Mediální soubory z příspěvků vzdálených uživatelů se ukládají do mezipaměti na vašem serveru. Pokud je nastaveno na kladnou hodnotu, budou média po zadaném počtu dní odstraněna. Pokud jsou mediální data vyžádána po jejich odstranění, budou znovu stažena, pokud je zdrojový obsah stále k dispozici. Vzhledem k omezením týkajícím se četnosti dotazů karet náhledů odkazů na weby třetích stran se doporučuje nastavit tuto hodnotu alespoň na 14 dní, jinak nebudou karty náhledů odkazů na vyžádání aktualizovány dříve. min_age: Uživatelé budou požádáni, aby při registraci potvrdili datum svého narození @@ -288,7 +289,8 @@ cs: content_cache_retention_period: Doba uchovávání vzdáleného obsahu custom_css: Vlastní CSS favicon: Favicon - local_live_feed_access: Přístup k live kanálům s lokálními příspěvky + landing_page: Úvodní stránka pro nové návštěvníky + local_live_feed_access: Přístup k živým kanálům s lokálními příspěvky local_topic_feed_access: Přístup ke kanálům s hashtagy a odkazy s lokálními příspěvky mascot: Vlastní maskot (zastaralé) media_cache_retention_period: Doba uchovávání mezipaměti médií diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index a4b4a13c04..6c5711aa5f 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -93,6 +93,7 @@ da: content_cache_retention_period: Alle indlæg fra andre servere (herunder fremhævelser og besvarelser) slettes efter det angivne antal dage uden hensyn til lokal brugerinteraktion med disse indlæg. Dette omfatter indlæg, hvor en lokal bruger har markeret dem som bogmærker eller favoritter. Private omtaler mellem brugere fra forskellige instanser vil også være tabt og umulige at gendanne. Brugen af denne indstilling er beregnet til særlige formål instanser og bryder mange brugerforventninger ved implementering til almindelig brug. custom_css: Man kan anvende tilpassede stilarter på Mastodon-webversionen. favicon: WEBP, PNG, GIF eller JPG. Tilsidesætter standard Mastodon favikonet på mobilenheder med et tilpasset ikon. + landing_page: Vælger, hvilken side nye besøgende ser, når de først ankommer til din server. Hvis du vælger "Trender", skal trends være aktiveret i Opdagelse-indstillingerne. Hvis du vælger "Lokalt feed", skal "Adgang til live feeds med lokale indlæg" være indstillet til "Alle" i Opdagelse-indstillingerne. mascot: Tilsidesætter illustrationen i den avancerede webgrænseflade. media_cache_retention_period: Mediefiler fra indlæg oprettet af eksterne brugere er cachet på din server. Når sat til positiv værdi, slettes medier efter det angivne antal dage. Anmodes om mediedata efter de er slettet, gendownloades de, hvis kildeindholdet stadig er tilgængeligt. Grundet begrænsninger på, hvor ofte linkforhåndsvisningskort forespørger tredjeparts websteder, anbefales det at sætte denne værdi til mindst 14 dage, ellers opdateres linkforhåndsvisningskort ikke efter behov før det tidspunkt. min_age: Brugere anmodes om at bekræfte deres fødselsdato under tilmelding @@ -286,6 +287,7 @@ da: content_cache_retention_period: Opbevaringsperiode for eksternt indhold custom_css: Tilpasset CSS favicon: Favikon + landing_page: Landingside for nye besøgende local_live_feed_access: Adgang til live feeds med lokale indlæg local_topic_feed_access: Adgang til hashtag- og link-feeds med lokale indlæg mascot: Tilpasset maskot (ældre funktion) diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index acfa81ae33..03dd292e0f 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -93,6 +93,7 @@ el: content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές αναφορές μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστοσελίδας του Mastodon. favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. + landing_page: Επιλέγει ποια σελίδα βλέπουν οι νέοι επισκέπτες όταν φτάνουν για πρώτη φορά στο διακομιστή σας. Αν επιλέξετε "Τάσεις", τότε οι τάσεις πρέπει να είναι ενεργοποιημένες στις Ρυθμίσεις Ανακάλυψης. Αν επιλέξετε "Τοπική ροή", τότε το "Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις" πρέπει να οριστεί σε "Όλοι" στις Ρυθμίσεις Ανακάλυψης. mascot: Παρακάμπτει την εικονογραφία στην προηγμένη διεπαφή ιστού. media_cache_retention_period: Τα αρχεία πολυμέσων από αναρτήσεις που γίνονται από απομακρυσμένους χρήστες αποθηκεύονται προσωρινά στο διακομιστή σου. Όταν οριστεί μια θετική τιμή, τα μέσα θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών. Αν τα δεδομένα πολυμέσων ζητηθούν μετά τη διαγραφή τους, θα γίνει ε, αν το πηγαίο περιεχόμενο είναι ακόμα διαθέσιμο. Λόγω περιορισμών σχετικά με το πόσο συχνά οι κάρτες προεπισκόπησης συνδέσμων συνδέονται σε ιστοσελίδες τρίτων, συνιστάται να ορίσεις αυτή την τιμή σε τουλάχιστον 14 ημέρες ή οι κάρτες προεπισκόπησης συνδέσμων δεν θα ενημερώνονται κατ' απάιτηση πριν από εκείνη την ώρα. min_age: Οι χρήστες θα κληθούν να επιβεβαιώσουν την ημερομηνία γέννησής τους κατά την εγγραφή @@ -286,6 +287,7 @@ el: content_cache_retention_period: Περίοδος διατήρησης απομακρυσμένου περιεχομένου custom_css: Προσαρμοσμένο CSS favicon: Favicon + landing_page: Σελίδα προσγείωσης για νέους επισκέπτες local_live_feed_access: Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις local_topic_feed_access: Πρόσβαση σε ροές ετικετών και συνδέσμων με τοπικές αναρτήσεις mascot: Προσαρμοσμένη μασκότ (απαρχαιωμένο) diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index 1fe68d5fb5..b58d916d64 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -93,6 +93,7 @@ es-AR: content_cache_retention_period: Todos los mensajes de otros servidores (incluyendo adhesiones y respuestas) se eliminarán después del número de días especificado, sin tener en cuenta la interacción del usuario local con esos mensajes. Esto incluye mensajes que un usuario local haya agregado a marcadores o los haya marcado como favoritos. Las menciones privadas entre usuarios de diferentes servidores también se perderán y también serán imposibles de restaurar. El uso de esta configuración está destinado a servidores de propósito especial y rompe muchas expectativas de los usuarios cuando se implementa para uso general. custom_css: Podés aplicar estilos personalizados a la versión web de Mastodon. favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicón predeterminado de Mastodon con uno personalizado. + landing_page: Selecciona qué página ven los nuevos visitantes cuando llegan por primera vez a tu servidor. Si seleccionas "Tendencias", entonces las tendencias deben estar habilitadas en la Configuración de Descubrimiento. Si selecciona "Cronología local", entonces "Acceso a las cronologías que destacan publicaciones locales" debe configurarse a "Todos" en la Configuración de Descubrimiento. mascot: Reemplaza la ilustración en la interface web avanzada. media_cache_retention_period: Los archivos de medios de mensajes publicados por usuarios remotos se almacenan en la memoria caché en tu servidor. Cuando se establece un valor positivo, los medios se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si es que el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlace consultan a sitios web de terceros, se recomienda establecer este valor a, al menos, 14 días, o las tarjetas de previsualización de enlaces no se actualizarán a pedido antes de ese momento. min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro @@ -286,6 +287,7 @@ es-AR: content_cache_retention_period: Período de retención de contenido remoto custom_css: CSS personalizado favicon: Favicón + landing_page: Página de inicio para nuevos visitantes local_live_feed_access: Acceso a líneas temporales en vivo, destacando mensajes locales local_topic_feed_access: Acceso a líneas temporales de etiquetas y enlaces, destacando mensajes locales mascot: Mascota personalizada (legado) diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index 1cbd2f4059..9494a93067 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -93,6 +93,7 @@ es-MX: content_cache_retention_period: Todas las publicaciones de otros servidores (incluyendo impuestos y respuestas) serán borrados después del número de días especificado, sin tener en cuenta cualquier interacción del usuario local con esas publicaciones. Esto incluye los mensajes que un usuario local haya marcado como favoritos. Las menciones privadas entre usuarios de diferentes instancias también se perderán y será imposible restaurarlas. El uso de esta configuración está pensado para instancias de propósito especial y rompe muchas expectativas de los usuarios cuando se implementa para uso general. custom_css: Puedes aplicar estilos personalizados a la versión web de Mastodon. favicon: WEBP, PNG, GIF o JPG. Reemplaza el icono predeterminado de Mastodon con un icono personalizado. + landing_page: Selecciona qué página ven los nuevos visitantes cuando llegan por primera vez a tu servidor. Si seleccionas "Tendencias", entonces las tendencias deben estar habilitadas en la Configuración de Descubrimiento. Si selecciona "Cronología local", entonces "Acceso a las cronologías que destacan publicaciones locales" debe configurarse a "Todos" en la Configuración de Descubrimiento. mascot: Reemplaza la ilustración en la interfaz web avanzada. media_cache_retention_period: Los archivos multimedia de las publicaciones realizadas por usuarios remotos se almacenan en caché en su servidor. Si se establece en un valor positivo, los archivos multimedia se eliminarán tras el número de días especificado. Si los datos multimedia se solicitan después de haber sido eliminados, se volverán a descargar, si el contenido de origen sigue estando disponible. Debido a las restricciones sobre la frecuencia con la que las tarjetas de previsualización de enlaces sondean sitios de terceros, se recomienda establecer este valor en al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese tiempo. min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento al registrarse @@ -286,6 +287,7 @@ es-MX: content_cache_retention_period: Periodo de conservación de contenidos remotos custom_css: CSS personalizado favicon: Favicon + landing_page: Página de inicio para nuevos visitantes local_live_feed_access: Acceso a las cronologías que destacan publicaciones locales local_topic_feed_access: Acceso a las etiquetas y enlaces en tendencia que destacan publicaciones locales mascot: Mascota personalizada (legado) diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 99aaf64279..5c431d0463 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -93,6 +93,7 @@ es: content_cache_retention_period: Todas las publicaciones de otros servidores (incluso impulsos y respuestas) se eliminarán después del número de días especificado, sin tener en cuenta la interacción del usuario local con esos mensajes. Esto incluye mensajes donde un usuario local los ha marcado como marcadores o favoritos. Las menciones privadas entre usuarios de diferentes instancias también se perderán sin posibilidad de recuperación. El uso de esta configuración está destinado a instancias de propósito especial, y rompe muchas expectativas de los usuarios cuando se implementa para un uso de propósito general. custom_css: Puedes aplicar estilos personalizados a la versión web de Mastodon. favicon: WEBP, PNG, GIF o JPG. Reemplaza el favicon predeterminado de Mastodon con un icono personalizado. + landing_page: Selecciona qué página ven los nuevos visitantes cuando llegan por primera vez a tu servidor. Si seleccionas "Tendencias", entonces las tendencias deben estar habilitadas en la Configuración de Descubrimiento. Si selecciona "Cronología local", entonces "Acceso a las cronologías que destacan publicaciones locales" debe configurarse a "Todos" en la Configuración de Descubrimiento. mascot: Reemplaza la ilustración en la interfaz web avanzada. media_cache_retention_period: Los archivos multimedia de las publicaciones creadas por usuarios remotos se almacenan en caché en tu servidor. Cuando se establece un valor positivo, estos archivos se eliminarán después del número especificado de días. Si los datos multimedia se solicitan después de eliminarse, se volverán a descargar, si el contenido fuente todavía está disponible. Debido a restricciones en la frecuencia con la que las tarjetas de previsualización de enlaces realizan peticiones a sitios de terceros, se recomienda establecer este valor a al menos 14 días, o las tarjetas de previsualización de enlaces no se actualizarán bajo demanda antes de ese momento. min_age: Se pedirá a los usuarios que confirmen su fecha de nacimiento durante el registro @@ -286,6 +287,7 @@ es: content_cache_retention_period: Período de retención de contenido remoto custom_css: CSS personalizado favicon: Favicon + landing_page: Página de inicio para nuevos visitantes local_live_feed_access: Acceso a las cronologías que destacan publicaciones locales local_topic_feed_access: Acceso a las etiquetas y enlaces en tendencia que destacan publicaciones locales mascot: Mascota personalizada (legado) diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml index 0f5424e417..14a97e07bc 100644 --- a/config/locales/simple_form.fo.yml +++ b/config/locales/simple_form.fo.yml @@ -93,6 +93,7 @@ fo: content_cache_retention_period: Allir postar frá øðrum ambætarum (íroknað stimbranir og svar) verða strikaði eftir ásetta talið av døgum, óansæð hvussu lokalir brúkarar hava samvirkað við hesar postar. Hetta fevnir eisini um postar, sum lokalir brúkarar hava bókamerkt ella yndismerkt. Privatar umrøður millum brúkarar frá ymiskum ambætarum verða eisini burturmistar og ómøguligar at endurskapa. Brúk av hesi stillingini er einans hugsað til serligar støður og oyðileggur nógv, sum brúkarar vænta av einum vanligum ambætara. custom_css: Tú kanst seta títt egna snið upp í net-útgávuni av Mastodon. favicon: WEBP, PNG, GIF ella JPG. Býtir vanligu Mastodon fav-ikonina um við eina ser-ikon. + landing_page: Velur hvørja síðu nýggj vitandi síggja tá tey koma á ambætaran hjá tær. Neyðugt er at rák eru gjørd virkin í Uppdagingarstillingum, um tú velur "Rák". Velur tú "Lokal rás" má "Atgongd til beinleiðis rásir við lokalum postum" vera sett til "Øll" í Uppdagingarstillingum. mascot: Skúgvar til viks myndprýðingina í framkomna vev-markamótinum. media_cache_retention_period: Miðlafílur frá postum, sum fjarbrúkarar hava gjørt, verða goymdir á tínum ambætara. Tá hetta er sett til eitt virði størri enn 0, so verða miðlafílurnar strikaðar eftir ásetta talið av døgum. Um miðladátur verða umbidnar eftir at tær eru strikaðar, verða tær tiknar innaftur á ambætaran, um keldutilfarið enn er tøkt. Vegna avmarkingar á hvussu ofta undanvísingarkort til leinki spyrja triðjapartsstøð, so verður mælt til at seta hetta virðið til í minsta lagi 14 dagar. Annars verða umbønir um dagføringar av undanvísingarkortum til leinki ikki gjørdar áðrenn hetta. min_age: Brúkarar verða spurdir um at vátta teirra føðingardag, tá tey skráseta seg @@ -286,6 +287,7 @@ fo: content_cache_retention_period: Tíðarskeið fyri varðveiðslu av fjartilfari custom_css: Serskilt CSS favicon: Favikon + landing_page: Heimasíða til nýggj vitjandi local_live_feed_access: Atgongd til beinleiðis rásir við lokalum postum local_topic_feed_access: Atgongd til frámerki og rásir við leinkjum við lokalum postum mascot: Serskildur maskottur (arvur) diff --git a/config/locales/simple_form.ga.yml b/config/locales/simple_form.ga.yml index 203e12b4ff..decbe02cec 100644 --- a/config/locales/simple_form.ga.yml +++ b/config/locales/simple_form.ga.yml @@ -93,6 +93,7 @@ ga: content_cache_retention_period: Scriosfar gach postáil ó fhreastalaithe eile (lena n-áirítear treisithe agus freagraí) tar éis an líon sonraithe laethanta, gan aird ar aon idirghníomhaíocht úsáideora áitiúil leis na postálacha sin. Áirítear leis seo postálacha ina bhfuil úsáideoir áitiúil tar éis é a mharcáil mar leabharmharcanna nó mar cheanáin. Caillfear tagairtí príobháideacha idir úsáideoirí ó chásanna éagsúla freisin agus ní féidir iad a athchóiriú. Tá úsáid an tsocraithe seo beartaithe le haghaidh cásanna sainchuspóra agus sáraítear go leor ionchais úsáideoirí nuair a chuirtear i bhfeidhm é le haghaidh úsáid ghinearálta. custom_css: Is féidir leat stíleanna saincheaptha a chur i bhfeidhm ar an leagan gréasáin de Mastodon. favicon: WEBP, PNG, GIF nó JPG. Sáraíonn sé an favicon Mastodon réamhshocraithe le deilbhín saincheaptha. + landing_page: Roghnaíonn sé seo an leathanach a fheiceann cuairteoirí nua nuair a shroicheann siad do fhreastalaí den chéad uair. Má roghnaíonn tú "Treochtaí", ní mór treochtaí a chumasú sna Socruithe Fionnachtana. Má roghnaíonn tú "Fotha Áitiúil", ní mór "Rochtain ar fhothaí beo ina bhfuil poist áitiúla" a shocrú go "Gach Duine" sna Socruithe Fionnachtana. mascot: Sáraíonn sé an léaráid san ardchomhéadan gréasáin. media_cache_retention_period: Déantar comhaid meán ó phoist a dhéanann cianúsáideoirí a thaisceadh ar do fhreastalaí. Nuair a bheidh luach dearfach socraithe, scriosfar na meáin tar éis an líon sonraithe laethanta. Má iarrtar na sonraí meán tar éis é a scriosadh, déanfar é a ath-íoslódáil, má tá an t-ábhar foinse fós ar fáil. Mar gheall ar shrianta ar cé chomh minic is atá cártaí réamhamhairc ag vótaíocht do shuíomhanna tríú páirtí, moltar an luach seo a shocrú go 14 lá ar a laghad, nó ní dhéanfar cártaí réamhamhairc naisc a nuashonrú ar éileamh roimh an am sin. min_age: Iarrfar ar úsáideoirí a ndáta breithe a dhearbhú le linn clárúcháin @@ -289,6 +290,7 @@ ga: content_cache_retention_period: Tréimhse choinneála inneachair cianda custom_css: CSS saincheaptha favicon: Favicon + landing_page: Leathanach tuirlingthe do chuairteoirí nua local_live_feed_access: Rochtain ar bheatha bheo ina bhfuil poist áitiúla local_topic_feed_access: Rochtain ar fhothaí hashtag agus nasc ina bhfuil poist áitiúla mascot: Mascóg saincheaptha (oidhreacht) diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index 9de34b6c81..5f96869d76 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -93,6 +93,7 @@ he: content_cache_retention_period: כל ההודעות משרתים אחרים (לרבות הדהודים ותגובות) ימחקו אחרי מספר ימים, ללא קשר לאינטראקציה של משתמשים מקומיים איתם. בכלל זה הודעות שהמתשתמשים המקומיים סימנו בסימניה או חיבוב. איזכורים פרטיים ("דיאם") בין משתמשים בין שרתים שונים יאבדו גם הם ולא תהיה אפשרות לשחזרם. השימוש באפשרות הזו מיועד לשרתים עם ייעוד מיוחד ושובר את ציפיותיהם של רב המשתמשים כאשר האפשרות מופעלת בשרת לשימוש כללי. custom_css: ניתן לבחור ערכות סגנון אישיות בגרסת הדפדפן של מסטודון. favicon: WEBP, PNG, GIF או JPG. גובר על "פאבאייקון" ברירת המחדל ומחליף אותו באייקון נבחר בדפדפן. + landing_page: בחירה בעמוד שיוצג ראשון למבקרים חדשים בביקור הראשון בשרת שלך. אם תבחרו "נושאים חמים", אזי הנושאים החמים צריכים להיות מאופשרים בהעדפות "תגליות". אם תבחרו "פיד מקומי", אז "גישה לפידים חיים המציגים הודעות מקומיות" חייב להיות מכוון למצב "כולם" בהעדפות תגליות. mascot: בחירת ציור למנשק הווב המתקדם. media_cache_retention_period: קבצי מדיה מהודעות שהגיעו משרתים רחוקים נשמרות על השרת שלך. כאשר יבחר פה מספר חיובי, המדיה תמחק לאחר מספר ימים כמצוין. אם המידע יבוקש שוב לאחר שנמחק, הוא יורד מחדש, אם המידע עדיין זמין בצד הרחוק. עקב מגבלות על תכיפות שליפת כרטיסי קדימון מאתרים מרוחקים, מומלץ לכוון את הערך ל־14 יום לפחות, או שכרטיסי קדימונים לא יעודכנו לפי דרישה לפני חלוף חלון הזמן הזה. min_age: משתמשיםות יתבקשו לאשר את תאריך הלידה בתהליך ההרשמה @@ -288,6 +289,7 @@ he: content_cache_retention_period: תקופת השמירה על תוכן חיצוני custom_css: CSS בהתאמה אישית favicon: סמל מועדפים (Favicon) + landing_page: דף נחיתה למבקרים חדשים local_live_feed_access: גישה לפידים חיים המציגים הודעות מקומיות local_topic_feed_access: גישה לפידים של תגיות וקישורים המציגים הודעות מקומיות mascot: סמל השרת (ישן) diff --git a/config/locales/simple_form.ia.yml b/config/locales/simple_form.ia.yml index 45fedf9daf..b231ff0ded 100644 --- a/config/locales/simple_form.ia.yml +++ b/config/locales/simple_form.ia.yml @@ -93,6 +93,7 @@ ia: content_cache_retention_period: Tote le messages de altere servitores (includite impulsos e responsas) essera delite post le numero de dies specificate, independentemente de tote interaction de usatores local con ille messages. Isto include le messages addite al marcapaginas o marcate como favorite per un usator local. Le mentiones private inter usatores de differente instantias tamben essera irrecuperabilemente perdite. Le uso de iste parametro es intendite pro instantias con scopos specific e viola multe expectationes de usatores si es implementate pro uso general. custom_css: Tu pote applicar stilos personalisate sur le version de web de Mastodon. favicon: WEBP, PNG, GIF o JPG. Supplanta le favicone predefinite de Mastodon con un icone personalisate. + landing_page: Selige le pagina presentate al nove visitatores al prime arrivata sur tu servitor. Si tu selige “Tendentias”, alora le tendentias debe esser activate in le Parametros de discoperta. Si tu selige “Canal local”, alora le option “Accesso a canales in vivo con messages local” debe esser mittite a “Omnes” in le Parametros de discoperta. mascot: Illo substitue le illustration in le interfacie web avantiate. media_cache_retention_period: Le files multimedial de messages producite per usatores distante se immagazina in cache sur tu servitor. Quando iste option es definite a un valor positive, tal files essera delite post le numero specificate de dies. Si alcuno requesta le datos multimedial post lor deletion, illos essera re-discargate si le contento original es ancora disponibile. Debite a limitationes sur le frequentia con que le cartas de previsualisation de ligamines se connecte al sitos de tertios, il es recommendate definir iste valor a al minus 14 dies, alteremente le previsualisationes de ligamines non essera actualisate sur demanda ante ille tempore. min_age: Le usatores debera confirmar lor data de nascentia durante le inscription @@ -286,6 +287,7 @@ ia: content_cache_retention_period: Periodo de retention del contento remote custom_css: CSS personalisate favicon: Favicon + landing_page: Pagina de arrivata pro nove visitatores local_live_feed_access: Accesso a canales in vivo con messages local local_topic_feed_access: Accesso a canales de hashtag e ligamines con messages local mascot: Personalisar le mascotte (hereditage) diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index 6b8ee6a118..ccaa8dd69d 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -286,6 +286,7 @@ is: content_cache_retention_period: Tímabil sem á að geyma fjartengt efni custom_css: Sérsniðið CSS favicon: Auðkennismynd + landing_page: Kynningarsíða fyrir nýja gesti local_live_feed_access: Aðgangur að beinum streymum, þar með töldum staðværum færslum local_topic_feed_access: Aðgangur að myllumerkjum og tengdum streymum, þar með töldum staðværum færslum mascot: Sérsniðið gæludýr (eldra) diff --git a/config/locales/simple_form.lad.yml b/config/locales/simple_form.lad.yml index de3063a17b..a04bf89f95 100644 --- a/config/locales/simple_form.lad.yml +++ b/config/locales/simple_form.lad.yml @@ -312,6 +312,7 @@ lad: terms_of_service_generator: choice_of_law: Legislasyon aplikavle domain: Domeno + min_age: Edad minima user: date_of_birth_1i: Diya date_of_birth_2i: Mez @@ -324,6 +325,8 @@ lad: name: Nombre permissions_as_keys: Permisos position: Priorita + username_block: + allow_with_approval: Permite enrejistrasyones kon aprovasyon webhook: events: Evenimientos kapasitados template: Modelo de kontenido diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index 3289cadb90..36522a6695 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -93,6 +93,7 @@ nl: content_cache_retention_period: Alle berichten van andere servers (inclusief boosts en reacties) worden verwijderd na het opgegeven aantal dagen, ongeacht enige lokale gebruikersinteractie met die berichten. Dit betreft ook berichten die een lokale gebruiker aan diens bladwijzers heeft toegevoegd of als favoriet heeft gemarkeerd. Privéberichten tussen gebruikers van verschillende servers gaan ook verloren en zijn onmogelijk te herstellen. Het gebruik van deze instelling is bedoeld voor servers die een speciaal doel dienen en overtreedt veel gebruikersverwachtingen wanneer deze voor algemeen gebruik wordt geïmplementeerd. custom_css: Je kunt aangepaste CSS toepassen op de webversie van deze Mastodon-server. favicon: WEBP, PNG, GIF of JPG. Vervangt de standaard Mastodon favicon met een aangepast pictogram. + landing_page: Selecteert welke pagina nieuwe bezoekers te zien krijgen wanneer ze voor het eerst op jouw server terechtkomen. Wanneer je ‘Trends’ selecteert, moeten trends ingeschakeld zijn onder 'Serverinstellingen > Ontdekken'. Als je ‘Lokale tijdlijn’ selecteert, moet ‘Toegang tot openbare lokale berichten’ worden ingesteld op ‘Iedereen’ onder 'Serverinstellingen > Ontdekken'. mascot: Overschrijft de illustratie in de geavanceerde webomgeving. media_cache_retention_period: Mediabestanden van berichten van externe gebruikers worden op jouw server in de cache opgeslagen. Indien ingesteld op een positieve waarde, worden media verwijderd na het opgegeven aantal dagen. Als de mediagegevens worden opgevraagd nadat ze zijn verwijderd, worden ze opnieuw gedownload wanneer de originele inhoud nog steeds beschikbaar is. Vanwege beperkingen op hoe vaak linkvoorbeelden sites van derden raadplegen, wordt aanbevolen om deze waarde in te stellen op ten minste 14 dagen. Anders worden linkvoorbeelden niet op aanvraag bijgewerkt. min_age: Gebruikers krijgen tijdens hun inschrijving de vraag om hun geboortedatum te bevestigen @@ -286,6 +287,7 @@ nl: content_cache_retention_period: Bewaartermijn voor externe inhoud custom_css: Aangepaste CSS favicon: Favicon + landing_page: Landingspagina voor nieuwe bezoekers local_live_feed_access: Toegang tot openbare lokale berichten local_topic_feed_access: Toegang tot overzicht met lokale hashtags en links mascot: Aangepaste mascotte (legacy) diff --git a/config/locales/simple_form.pt-BR.yml b/config/locales/simple_form.pt-BR.yml index 4aa2d15d30..605bcf8d98 100644 --- a/config/locales/simple_form.pt-BR.yml +++ b/config/locales/simple_form.pt-BR.yml @@ -44,7 +44,7 @@ pt-BR: bot: Sinaliza aos outros de que essa conta executa principalmente ações automatizadas e pode não ser monitorada context: Um ou mais contextos onde o filtro deve atuar current_password: Para fins de segurança, digite a senha da conta atual - current_username: Para confirmar, digite o nome de usuário da conta atual + current_username: Para confirmar, entre com nome de usuário da conta atual digest: Enviado apenas após um longo período de inatividade com um resumo das menções recebidas durante ausência email: Você receberá um e-mail de confirmação header: WEBP, PNG, GIF ou JPG. No máximo %{size}. Será reduzido para %{dimensions}px @@ -56,6 +56,8 @@ pt-BR: scopes: Quais APIs o aplicativo vai ter permissão de acessar. Se você selecionar uma autorização de alto nível, você não precisa selecionar individualmente os outros. setting_aggregate_reblogs: Não mostrar novos impulsos para publicações que já foram impulsionadas recentemente (afeta somente os impulsos mais recentes) setting_always_send_emails: Normalmente, as notificações por e-mail não serão enviadas enquanto você estiver usando ativamente o Mastodon + setting_default_quote_policy_private: Publicações exclusivas de seguidores criadas no Mastodon não podem ser citadas por outras pessoas. + setting_default_quote_policy_unlisted: Quando as pessoas citarem você, suas publicações também ficarão ocultas da linha do tempo. setting_default_sensitive: Mídia sensível está oculta por padrão e pode ser revelada com um clique setting_display_media_default: Sempre ocultar mídia sensível setting_display_media_hide_all: Sempre ocultar todas as mídias @@ -235,6 +237,7 @@ pt-BR: setting_default_privacy: Visibilidade da publicação setting_default_quote_policy: Quem pode citar setting_default_sensitive: Sempre marcar mídia como sensível + setting_delete_modal: Avise-me antes de apagar uma publicação setting_disable_hover_cards: Desativar visualização de perfil ao passar o mouse por cima setting_disable_swiping: Desabilitar movimentos deslizantes setting_display_media: Exibição das mídias @@ -244,6 +247,7 @@ pt-BR: setting_emoji_style: Estilo de emoji setting_expand_spoilers: Sempre expandir toots com Aviso de Conteúdo setting_hide_network: Ocultar suas relações + setting_missing_alt_text_modal: Avise-me antes de publicar mídia sem texto alternado setting_reduce_motion: Reduzir animações setting_system_font_ui: Usar fonte padrão do sistema setting_system_scrollbars_ui: Usar barra de rolagem padrão do sistema @@ -277,12 +281,17 @@ pt-BR: content_cache_retention_period: Período de retenção de conteúdo remoto custom_css: CSS personalizável favicon: Favicon + landing_page: Página inicial para novos visitantes + local_live_feed_access: Acessar feeds ao vivo com destaque em publicações locais + local_topic_feed_access: Acessar hasthtag e endereços de feed com destaque em publicações locais mascot: Mascote personalizado (legado) media_cache_retention_period: Período de retenção do cachê de mídia min_age: Requisito de idade mínimia peers_api_enabled: Publicar lista de instâncias de servidor descobertas na API profile_directory: Ativar diretório de perfis registrations_mode: Quem pode se inscrever + remote_live_feed_access: Acessar feeds ao vivo com destaque em publicações antigas + remote_topic_feed_access: Acessar hasthtag e endereços de feed com destaque em publicações antigas require_invite_text: Exigir uma razão para entrar show_domain_blocks: Mostrar domínios bloqueados show_domain_blocks_rationale: Mostrar por que domínios foram bloqueados diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 07b8010555..e8aa2692ec 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -93,6 +93,7 @@ pt-PT: content_cache_retention_period: Todas as publicações de outros servidores (incluindo partilhas e respostas) serão eliminadas após o número de dias especificado, independentemente de qualquer interação do utilizador local com essas publicações. Isto inclui mensagens em que um utilizador local as tenha salvo ou adicionado aos favoritos. As menções privadas entre utilizadores de instâncias diferentes também se perderão e serão impossíveis de recuperar. A utilização desta definição destina-se a instâncias para fins especiais e quebra muitas expectativas dos utilizadores quando implementada para utilização geral. custom_css: Pode aplicar estilos personalizados na versão web do Mastodon. favicon: WEBP, PNG, GIF ou JPG. Substitui o ícone de favorito padrão do Mastodon por um ícone personalizado. + landing_page: Seleciona a página que os novos visitantes veem quando chegam ao seu servidor pela primeira vez. Se selecionar «Tendências», então as tendências precisam estar ativas nas Definições de Descoberta. Se selecionar «Cronologia local», então «Acesso a cronologias com publicações locais em destaque» precisa de estar definido como «Todos» nas Definições de Descoberta. mascot: Sobrepõe-se à ilustração na interface web avançada. media_cache_retention_period: Os ficheiros multimédia de publicações feitas por utilizadores remotos são armazenados em cache no seu servidor. Quando definido para um valor positivo, os ficheiros multimédia serão eliminados após o número de dias especificado. Se os ficheiros multimédia forem solicitados depois de terem sido eliminados, serão transferidos novamente, se o conteúdo de origem ainda estiver disponível. Devido a restrições sobre a frequência com que os cartões de pré-visualização de hiperligação pesquisam sites de terceiros, recomenda-se que este valor seja definido para, pelo menos, 14 dias, ou os cartões de pré-visualização de hiperligação não serão atualizados a pedido antes desse período. min_age: Os utilizadores serão convidados a confirmar a sua data de nascimento durante o processo de inscrição @@ -286,6 +287,7 @@ pt-PT: content_cache_retention_period: Período de retenção de conteúdos remotos custom_css: CSS personalizado favicon: Ícone de favoritos + landing_page: Página inicial para novos visitantes local_live_feed_access: Acesso a cronologias com publicações locais em destaque local_topic_feed_access: Acesso a cronologias de etiquetas e hiperligações de publicações locais em destaque mascot: Mascote personalizada (legado) diff --git a/config/locales/simple_form.sq.yml b/config/locales/simple_form.sq.yml index 23f8481425..9df506083b 100644 --- a/config/locales/simple_form.sq.yml +++ b/config/locales/simple_form.sq.yml @@ -92,6 +92,7 @@ sq: content_cache_retention_period: Krejt postimet prej shërbyesve të tjerë (përfshi përforcime dhe përgjigje) do të fshihen pas numrit të caktuar të ditëve, pa marrë parasysh çfarëdo ndërveprimi përdoruesi me këto postime. Kjo përfshin postime kur një përdorues vendor u ka vënë shenjë si faqerojtës, ose të parapëlqyer. Do të humbin gjithashtu dhe përmendje private mes përdoruesish nga instanca të ndryshme dhe s’do të jetë e mundshme të rikthehen. Përdorimi i këtij rregullimi është menduar për instanca me qëllim të caktuar dhe ndërhyn në çka presin mjaft përdorues, kur sendërtohet për përdorim të përgjithshëm. custom_css: Stile vetjakë mund të aplikoni në versionin web të Mastodon-it. favicon: WEBP, PNG, GIF, ose JPG. Anashkalon favikonën parazgjedhje Mastodon me një ikonë vetjake. + landing_page: Përzgjedh cilën faqe shohin vizitorët e rinj, kur vijnë për herë të parë në shërbyesin tuaj. Nëse përzgjidhni “Në modë”, atëherë “në modë” duhet aktivizuar te Rregullime për Zbulime. Nëse përzgjidhni “Prurje vendore”, atëherë “Hyrje te prurje vendore që përmbajnë postime vendore” duhet vënë si “Gjithkush”, te Rregullime për Zbulime. mascot: Anashkalon ilustrimin te ndërfaqja web e thelluar. media_cache_retention_period: Kartela media nga postime të bëra nga përdorues të largët ruhen në një fshehtinë në shërbyesin tuaj. Kur i jepet një vlerë pozitive, media do të fshihet pas numrit të dhënë të ditëve. Nëse të dhënat e medias duhen pas fshirjes, do të rishkarkohen, nëse lënda burim mund të kihet ende. Për shkak kufizimesh mbi sa shpesh skeda paraparjesh lidhjesh ndërveprojnë me sajte palësh të treta, rekomandohet të vihet kjo vlerë të paktën 14 ditë, ose skedat e paraparjes së lidhje s’do të përditësohen duke e kërkuar para asaj kohe. min_age: Përdoruesve do t’ju kërkohet gjatë regjistrimit të ripohojnë datën e lindjes @@ -285,6 +286,7 @@ sq: content_cache_retention_period: Periudhë mbajtjeje lënde të largët custom_css: CSS Vetjake favicon: Favikonë + landing_page: Faqe mbërritje për vizitorë të rinj local_live_feed_access: Hyrje te prurje të atypëratyshme që përmbajnë postime vendore local_topic_feed_access: Hyrje te prurje hashtag-ësh dhe lidhjesh që përmbajnë postime vendore mascot: Simbol vetjak (e dikurshme) diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 05367bdf99..f64c12cf90 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -93,6 +93,7 @@ tr: content_cache_retention_period: Diğer sunuculardaki (öne çıkarma ve yanıtlar da dahil olmak üzere) tüm gönderiler belirlenen gün sonunda, yerel bir kullanıcının etkileşimine bakılmadan, silinecektir. Yerel bir kullanıcının yerimlerine veya favorilerine eklediği gönderiler de dahildir. Farklı sunuculardaki kullanıcılar arasındaki özel bahsetmeler de kaybolacak ve geri getirilmeleri mümkün olmayacaktır. Bu ayarın kullanımı özel amaçlı sunucular içindir ve genel amaçlı kullanımda etkinleştirildiğinde kullanıcı beklentilerini karşılamayabilir. custom_css: Mastodon'un web sürümüne özel biçimler uygulayabilirsiniz. favicon: WEBP, PNG, GIF veya JPG. Varsayılan Mastodon simgesini isteğe bağlı bir simgeyle değiştirir. + landing_page: Yeni ziyaretçilerin sunucunuza ilk geldiklerinde görecekleri sayfayı seçer. "Öne çıkanlar" seçeneğini seçerseniz, Keşif Ayarlarında öne çıkanların etkinleştirilmesi gerekir. "Yerel akış" seçeneğini seçerseniz, Keşif Ayarlarında "Yerel gönderileri içeren canlı akışlara erişim" seçeneğinin "Herkes" olarak ayarlanması gerekir. mascot: Gelişmiş web arayüzündeki illüstrasyonu geçersiz kılar. media_cache_retention_period: Uzak kullanıcıların gönderilerindeki ortam dosyaları sunucunuzda önbelleklenir. Pozitif bir değer verildiğinde, ortam dosyaları belirlenen gün sonunda silinecektir. Eğer ortam dosyaları silindikten sonra istenirse, kaynak içerik hala mevcutsa, tekrar indirilecektir. Bağlantı önizleme kartlarının üçüncü parti siteleri yoklamasına ilişkin kısıtlamalar nedeniyle, bu değeri en azından 14 gün olarak ayarlamanız önerilir, yoksa bağlantı önizleme kartları bu süreden önce isteğe bağlı olarak güncellenmeyecektir. min_age: Kullanıcılardan kayıt olurken doğum tarihlerini doğrulamaları istenecektir @@ -286,6 +287,7 @@ tr: content_cache_retention_period: Uzak içerik saklama süresi custom_css: Özel CSS favicon: Yer imi simgesi + landing_page: Yeni ziyaretçiler için giriş sayfası local_live_feed_access: Yerel gönderileri ön plana çıkaran canlı akışlara erişim local_topic_feed_access: Yerel gönderileri ön plana çıkaran etiket ve bağlantı akışlarına erişim mascot: Özel maskot (eski) diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index eb2610bdbc..8432ded89d 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -93,6 +93,7 @@ vi: content_cache_retention_period: Tất cả tút từ các máy chủ khác (bao gồm cả đăng lại và trả lời) sẽ bị xóa sau số ngày được chỉ định mà không tính đến bất kỳ tương tác nào của người dùng cục bộ với các tút đó. Điều này bao gồm các tút mà người dùng cục bộ đã đánh dấu nó là dấu trang hoặc mục yêu thích. Những lượt nhắc riêng tư giữa những người dùng từ các máy chủ khác nhau cũng sẽ bị mất và không thể khôi phục. Việc sử dụng cài đặt này dành cho các trường hợp có mục đích đặc biệt và phá vỡ nhiều kỳ vọng của người dùng khi được triển khai cho mục đích sử dụng chung. custom_css: Bạn có thể tùy chỉnh phong cách trên bản web của Mastodon. favicon: WEBP, PNG, GIF hoặc JPG. Dùng favicon Maston tùy chỉnh. + landing_page: Chọn trang mà khách truy cập mới sẽ thấy khi họ lần đầu truy cập máy chủ của bạn. Nếu bạn chọn "Xu hướng", thì cần bật xu hướng trong Cài đặt Khám phá. Nếu bạn chọn "Bảng tin máy chủ", thì cần đặt "Truy cập vào nguồn cấp dữ liệu trực tiếp có bài đăng cục bộ" thành "Mọi người" trong Cài đặt Khám phá. mascot: Ghi đè hình minh họa trong giao diện web nâng cao. media_cache_retention_period: Các tệp phương tiện từ các tút do người dùng máy chủ khác thực hiện sẽ được lưu vào bộ đệm trên máy chủ của bạn. Khi được đặt thành giá trị dương, phương tiện sẽ bị xóa sau số ngày được chỉ định. Nếu dữ liệu phương tiện được yêu cầu sau khi bị xóa, dữ liệu đó sẽ được tải xuống lại nếu nội dung nguồn vẫn còn. Do những hạn chế về tần suất thẻ xem trước liên kết thăm dò ý kiến ​​các trang web của bên thứ ba, bạn nên đặt giá trị này thành ít nhất 14 ngày, nếu không thẻ xem trước liên kết sẽ không được cập nhật theo yêu cầu trước thời gian đó. min_age: Thành viên sẽ được yêu cầu xác nhận ngày sinh của họ trong quá trình đăng ký @@ -285,6 +286,7 @@ vi: content_cache_retention_period: Khoảng thời gian lưu giữ nội dung máy chủ khác custom_css: Tùy chỉnh CSS favicon: Favicon + landing_page: Trang mở đầu dành cho khách ghé thăm local_live_feed_access: Truy cập bảng tin gồm những tút của máy chủ local_topic_feed_access: Truy cập hashtag và bảng tin liên kết gồm những tút của máy chủ mascot: Tùy chỉnh linh vật (kế thừa) diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index a205039f24..c740aba19f 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -93,6 +93,7 @@ zh-CN: content_cache_retention_period: 来自其它实例的所有嘟文(包括转嘟与回复)都将在指定天数后被删除,不论本实例用户是否与这些嘟文产生过交互。这包括被本实例用户喜欢和收藏的嘟文。实例间用户的私下提及也将丢失并无法恢复。此设置针对的是特殊用途的实例,用于一般用途时会打破许多用户的期望。 custom_css: 你可以为网页版 Mastodon 应用自定义样式。 favicon: WEBP、PNG、GIF 或 JPG。使用自定义图标覆盖 Mastodon 的默认图标。 + landing_page: 选择新访客首次访问您的服务器时看到的页面。 如果选择“热门”,则需要在“发现”设置中启用热门趋势。 如果选择“本站动态”,则在“发现”设置中“展示本站嘟文的实时动态访问权限”一项需要设置为“所有人”。 mascot: 覆盖高级网页界面中的绘图形象。 media_cache_retention_period: 来自外站用户嘟文的媒体文件将被缓存到你的实例上。当该值被设为正值时,缓存的媒体文件将在指定天数后被清除。如果媒体文件在被清除后重新被请求,且源站内容仍然可用,它将被重新下载。由于链接预览卡拉取第三方站点的频率受到限制,建议将此值设置为至少 14 天,如果小于该值,链接预览卡将不会按需更新。 min_age: 用户注册时必须确认出生日期 @@ -285,6 +286,7 @@ zh-CN: content_cache_retention_period: 外站内容保留期 custom_css: 自定义 CSS favicon: Favicon + landing_page: 新访客的主页 local_live_feed_access: 展示本站嘟文的实时动态访问权限 local_topic_feed_access: 展示本站嘟文的话题标签及实时动态访问权限 mascot: 自定义吉祥物(旧) diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index 4704199c56..181965d514 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -93,6 +93,7 @@ zh-TW: content_cache_retention_period: 所有來自其他伺服器之嘟文(包括轉嘟與回嘟)將於指定之天數後自動刪除,不論這些嘟文與本地使用者間的任何互動。這將包含本地使用者已標記為書籤或最愛之嘟文。不同站點使用者間之私訊亦將遺失且不可回復。此設定應適用於特殊情況,若常規使用將超乎多數使用者預期。 custom_css: 您於 Mastodon 網頁版本中能套用客製化風格。 favicon: WEBP、PNG、GIF、或 JPG。使用自訂圖示替代預設 Mastodon favicon 圖示。 + landing_page: 選擇當新訪客第一次造訪您伺服器時所見之頁面。若您選擇「熱門趨勢」,則該功能必須於探索設定中啟用。若您選擇「本站時間軸」,則探索設定中「允許瀏覽本站嘟文之即時內容」功能必須設定為「任何人」。 mascot: 覆寫進階網頁介面中的圖例。 media_cache_retention_period: 來自遠端伺服器嘟文中之多媒體內容將快取於您的伺服器。當設定為正值時,這些多媒體內容將於指定之天數後自您的儲存空間中自動刪除。若多媒體資料於刪除後被請求,且原始內容仍可存取,它們將被重新下載。由於連結預覽中第三方網站查詢頻率限制,建議將其設定為至少 14 日,否則於此之前連結預覽將不被即時更新。 min_age: 使用者將於註冊時被要求確認他們的生日 @@ -285,6 +286,7 @@ zh-TW: content_cache_retention_period: 遠端內容保留期限 custom_css: 自訂 CSS favicon: 網站圖示 (Favicon) + landing_page: 新訪客之登陸頁面 local_live_feed_access: 允許瀏覽本站嘟文之即時內容 local_topic_feed_access: 允許瀏覽本站嘟文之主題標籤與連結 mascot: 自訂吉祥物 (legacy) diff --git a/config/locales/sq.yml b/config/locales/sq.yml index 20830e4ded..6aa95821d4 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -847,6 +847,11 @@ sq: authenticated: Vetëm përdorues të mirëfilltësuar disabled: Lyp doemos rol specifik përdoruesi public: Kushdo + landing_page: + values: + about: Mbi + local_feed: Prurje vendore + trends: Në modë registrations: moderation_recommandation: Ju lutemi, sigurohuni si keni një ekip adekuat dhe reagues moderimi, përpara se të hapni regjistrimet për këdo! preamble: Kontrolloni cilët mund të krijojnë llogari në shërbyesin tuaj. diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 8f2894e0c1..3cedf77bbe 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -796,6 +796,8 @@ tr: view_dashboard_description: Kullanıcıların ana panele ve çeşitli ölçütlere erişmesine izin verir view_devops: DevOps view_devops_description: Kullanıcıların Sidekiq ve pgHero panellerine erişmesine izin verir + view_feeds: Canlı ve konu akışlarını görüntüle + view_feeds_description: Kullanıcıların sunucu ayarlarından bağımsız olarak canlı ve konu akışlarına erişmelerini sağlar title: Roller rules: add_new: Kural ekle @@ -851,7 +853,13 @@ tr: feed_access: modes: authenticated: Sadece yetkilendirilmiş kullanıcılar + disabled: Belirli kullanıcı rolü gerekir public: Herkes + landing_page: + values: + about: Hakkında + local_feed: Yerel akış + trends: Öne çıkanlar registrations: moderation_recommandation: Lütfen kayıtları herkese açmadan önce yeterli ve duyarlı bir denetleyici ekibine sahip olduğunuzdan emin olun! preamble: Sunucunuzda kimin hesap oluşturabileceğini denetleyin. diff --git a/config/locales/vi.yml b/config/locales/vi.yml index fe2a2183f8..0d3866f652 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -841,6 +841,11 @@ vi: authenticated: Chỉ những người dùng đã xác minh disabled: Yêu cầu vai trò người dùng cụ thể public: Mọi người + landing_page: + values: + about: Giới thiệu + local_feed: Bảng tin máy chủ + trends: Xu hướng registrations: moderation_recommandation: Vui lòng đảm bảo rằng bạn có một đội ngũ kiểm duyệt và phản ứng nhanh trước khi mở đăng ký cho mọi người! preamble: Kiểm soát những ai có thể tạo tài khoản trên máy chủ của bạn. diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 8245d9716b..e7385e392e 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -838,6 +838,11 @@ zh-CN: modes: authenticated: 仅已登录用户 public: 所有人 + landing_page: + values: + about: 关于 + local_feed: 本站动态 + trends: 热门 registrations: moderation_recommandation: 在向所有人开放注册之前,请确保你拥有一个人手足够且反应迅速的管理团队! preamble: 控制谁可以在你的服务器上创建账号。 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 5fb797b648..6c56aaf90f 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -843,6 +843,11 @@ zh-TW: authenticated: 僅限已登入之使用者 disabled: 需要特定使用者權限 public: 任何人 + landing_page: + values: + about: 關於 + local_feed: 本站時間軸 + trends: 熱門趨勢 registrations: moderation_recommandation: 對所有人開放註冊之前,請確保您有人手充足且反應靈敏的管理員團隊! preamble: 控制誰能於您伺服器上建立帳號。 diff --git a/package.json b/package.json index 49bfee461b..5288335b0f 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "eslint-import-resolver-typescript": "^4.2.5", "eslint-plugin-formatjs": "^5.3.1", "eslint-plugin-import": "~2.32.0", - "eslint-plugin-jsdoc": "^54.0.0", + "eslint-plugin-jsdoc": "^60.0.0", "eslint-plugin-jsx-a11y": "~6.10.2", "eslint-plugin-promise": "~7.2.1", "eslint-plugin-react": "^7.37.4", diff --git a/spec/lib/activitypub/activity/update_spec.rb b/spec/lib/activitypub/activity/update_spec.rb index b829f3a5ad..d905f68d8f 100644 --- a/spec/lib/activitypub/activity/update_spec.rb +++ b/spec/lib/activitypub/activity/update_spec.rb @@ -149,18 +149,17 @@ RSpec.describe ActivityPub::Activity::Update do shared_examples 'updates counts' do it 'updates the reblog count' do - expect(status.untrusted_reblogs_count).to eq reblogs + expect { subject.perform }.to change { status.reload.untrusted_reblogs_count }.to(reblogs) end it 'updates the favourites count' do - expect(status.untrusted_favourites_count).to eq favourites + expect { subject.perform }.to change { status.reload.untrusted_favourites_count }.to(favourites) end end context 'with an implicit update' do before do status.update!(uri: ActivityPub::TagManager.instance.uri_for(status)) - subject.perform end it_behaves_like 'updates counts' @@ -173,11 +172,89 @@ RSpec.describe ActivityPub::Activity::Update do before do status.update!(uri: ActivityPub::TagManager.instance.uri_for(status)) - subject.perform end it_behaves_like 'updates counts' end end + + context 'with an Article object' do + let(:updated) { nil } + let(:favourites) { 50 } + let(:reblogs) { 100 } + + let!(:status) do + Fabricate( + :status, + uri: 'https://example.com/statuses/article', + account: sender, + text: "

Future of the Fediverse

\n\n

Guest article by John Mastodon

The fediverse is great reading this you will find out why!

" + ) + end + + let(:json) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'foo', + type: 'Update', + actor: sender.uri, + object: { + type: 'Article', + id: status.uri, + name: 'Future of the Fediverse', + summary: '

Guest article by Jane Mastodon

The fediverse is great reading this you will find out why!

', + content: 'Foo', + updated: updated, + likes: { + id: "#{status.uri}/likes", + type: 'Collection', + totalItems: favourites, + }, + shares: { + id: "#{status.uri}/shares", + type: 'Collection', + totalItems: reblogs, + }, + }, + }.with_indifferent_access + end + + shared_examples 'updates counts' do + it 'updates the reblog count' do + expect { subject.perform }.to change { status.reload.untrusted_reblogs_count }.to(reblogs) + end + + it 'updates the favourites count' do + expect { subject.perform }.to change { status.reload.untrusted_favourites_count }.to(favourites) + end + end + + context 'with an implicit update' do + before do + status.update!(uri: ActivityPub::TagManager.instance.uri_for(status)) + end + + it_behaves_like 'updates counts' + end + + context 'with an explicit update' do + let(:favourites) { 150 } + let(:reblogs) { 200 } + let(:updated) { Time.now.utc.iso8601 } + + before do + status.update!(uri: ActivityPub::TagManager.instance.uri_for(status)) + end + + it_behaves_like 'updates counts' + + it 'changes the contents as expected' do + expect { subject.perform } + .to(change { status.reload.text }) + + expect(status.text).to start_with("

Future of the Fediverse

\n\n

Guest article by Jane Mastodon

The fediverse is great reading this you will find out why!

") + end + end + end end end diff --git a/spec/lib/extractor_spec.rb b/spec/lib/extractor_spec.rb index bc3ee8ac49..e1a57d5788 100644 --- a/spec/lib/extractor_spec.rb +++ b/spec/lib/extractor_spec.rb @@ -35,12 +35,24 @@ RSpec.describe Extractor do end describe 'extract_hashtags_with_indices' do - it 'returns an empty array if it does not have #' do + it 'returns an empty array if it does not have # or #' do text = 'a string without hash sign' extracted = described_class.extract_hashtags_with_indices(text) expect(extracted).to eq [] end + it 'returns hashtags preceded by an ASCII hash' do + text = 'hello #world' + extracted = described_class.extract_hashtags_with_indices(text) + expect(extracted).to eq [{ hashtag: 'world', indices: [6, 12] }] + end + + it 'returns hashtags preceded by a full-width hash' do + text = 'hello #world' + extracted = described_class.extract_hashtags_with_indices(text) + expect(extracted).to eq [{ hashtag: 'world', indices: [6, 12] }] + end + it 'does not exclude normal hash text before ://' do text = '#hashtag://' extracted = described_class.extract_hashtags_with_indices(text) diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index 18378c000d..d41d3a9e21 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -84,6 +84,10 @@ RSpec.describe Tag do expect(subject.match('this is #aesthetic').to_s).to eq '#aesthetic' end + it 'matches #foo' do + expect(subject.match('this is #foo').to_s).to eq '#foo' + end + it 'matches digits at the start' do expect(subject.match('hello #3d').to_s).to eq '#3d' end diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 249abc2440..ed41e54206 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -248,6 +248,29 @@ RSpec.describe '/api/v1/statuses' do end end + context 'with a quote of a reblog' do + let(:quoted_status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16) } + let(:reblog) { Fabricate(:status, reblog: quoted_status) } + let(:params) do + { + status: 'Hello world, this is a self-quote', + quoted_status_id: reblog.id, + } + end + + it 'returns a quote post, as well as rate limit headers', :aggregate_failures do + subject + + expect(response).to have_http_status(200) + expect(response.content_type) + .to start_with('application/json') + expect(response.parsed_body[:quote]).to be_present + expect(response.parsed_body[:quote][:quoted_status][:id]).to eq quoted_status.id.to_s + expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s + expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s + end + end + context 'with a self-quote post and a CW but no text' do let(:quoted_status) { Fabricate(:status, account: user.account) } let(:params) do diff --git a/spec/requests/signature_verification_spec.rb b/spec/requests/signature_verification_spec.rb index eccb2babc9..3119138a0a 100644 --- a/spec/requests/signature_verification_spec.rb +++ b/spec/requests/signature_verification_spec.rb @@ -352,34 +352,7 @@ RSpec.describe 'signature verification concern' do end end - # TODO: Remove when feature is enabled - context 'with an HTTP Message Signature (final RFC version) when support is disabled' do - before { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) } - - context 'with a valid signature on a GET request' do - let(:signature_input) do - 'sig1=("@method" "@target-uri");created=1703066400;keyid="https://remote.domain/users/bob#main-key"' - end - let(:signature_header) do - 'sig1=:WfM6q/qBqhUyqPUDt9metjadJGtLLpmMTBzk/t+R3byKe4/TGAXC6vBB/M6NsD5qv8GCmQGtisCMQxJQO0IGODGzi+Jv+eqDJ50agMVXNV6nUOzY44c4/XTPoI98qyx1oEMa4Hefy3vSYKq96iDVAc+RDLCMTeGP3wn9wizjD1SNmU0RZI1bTB+eCkywMP9mM5zXzUOYF+Qkuf+WdEpPR1XUGPlnqfdvPalcKVfaI/VThBjI91D/lmUGoa69x4EBEHM+aJmW6086e7/dVh+FndKkdGfXslZXFZKi2flTGQZgEWLn948SqAaJQROkJg8B14Sb1NONS1qZBhK3Mum8Pg==:' # rubocop:disable Layout/LineLength - end - - it 'cannot verify signature', :aggregate_failures do - get '/activitypub/signature_required', headers: { - 'Host' => 'www.example.com', - 'Signature-Input' => signature_input, - 'Signature' => signature_header, - } - - expect(response).to have_http_status(401) - expect(response.parsed_body).to match( - error: 'Error parsing signature parameters' - ) - end - end - end - - context 'with an HTTP Message Signature (final RFC version)', feature: :http_message_signatures do + context 'with an HTTP Message Signature (final RFC version)' do context 'with a known account' do let!(:actor) { Fabricate(:account, domain: 'remote.domain', uri: 'https://remote.domain/users/bob', private_key: nil, public_key: actor_keypair.public_key.to_pem) } diff --git a/streaming/database.js b/streaming/database.js index 553c9149cc..9dd7a97cfe 100644 --- a/streaming/database.js +++ b/streaming/database.js @@ -65,7 +65,7 @@ export function configFromEnv(env, environment) { if (typeof parsedUrl.ssl === 'boolean') { baseConfig.ssl = parsedUrl.ssl; } else if (typeof parsedUrl.ssl === 'object' && !Array.isArray(parsedUrl.ssl) && parsedUrl.ssl !== null) { - /** @type {Record} */ + /** @type {Record} */ const sslOptions = parsedUrl.ssl; baseConfig.ssl = {}; diff --git a/streaming/index.js b/streaming/index.js index 492491f04f..af3cae8d68 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -19,6 +19,7 @@ import * as Redis from './redis.js'; import { isTruthy, normalizeHashtag, firstParam } from './utils.js'; const environment = process.env.NODE_ENV || 'development'; +const PERMISSION_VIEW_FEEDS = 0x0000000000100000; // Correctly detect and load .env or .env.production file based on environment: const dotenvFile = environment === 'production' ? '.env.production' : '.env'; @@ -44,6 +45,18 @@ initializeLogLevel(process.env, environment); * @property {string[]} scopes * @property {string} accountId * @property {string[]} chosenLanguages + * @property {number} permissions + */ + +/** + * @typedef {http.IncomingMessage & ResolvedAccount & { + * path: string + * query: Record + * remoteAddress?: string + * cachedFilters: unknown + * scopes: string[] + * necessaryScopes: string[] + * }} Request */ @@ -52,8 +65,8 @@ initializeLogLevel(process.env, environment); * from redis and when receiving a message from a client over a websocket * connection, this is why it accepts a `req` argument. * @param {string} json - * @param {any?} req - * @returns {Object.|null} + * @param {Request?} req + * @returns {Object.|null} */ const parseJSON = (json, req) => { try { @@ -170,6 +183,7 @@ const startServer = async () => { let resolvedAccount; try { + // @ts-expect-error resolvedAccount = await accountFromRequest(request); } catch (err) { // Unfortunately for using the on('upgrade') setup, we need to manually @@ -220,7 +234,7 @@ const startServer = async () => { }); /** - * @type {Object.): void>>} + * @type {Object.): void>>} */ const subs = {}; @@ -338,7 +352,7 @@ const startServer = async () => { }; /** - * @param {http.IncomingMessage & ResolvedAccount} req + * @param {Request} req * @param {string[]} necessaryScopes * @returns {boolean} */ @@ -347,11 +361,11 @@ const startServer = async () => { /** * @param {string} token - * @param {any} req + * @param {Request} req * @returns {Promise} */ const accountFromToken = async (token, req) => { - const result = await pgPool.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id INNER JOIN accounts ON accounts.id = users.account_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE AND accounts.suspended_at IS NULL LIMIT 1', [token]); + const result = await pgPool.query('SELECT oauth_access_tokens.id, oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes, COALESCE(user_roles.permissions, 0) AS permissions FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id INNER JOIN accounts ON accounts.id = users.account_id LEFT OUTER JOIN user_roles ON user_roles.id = users.role_id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL AND users.disabled IS FALSE AND accounts.suspended_at IS NULL LIMIT 1', [token]); if (result.rows.length === 0) { throw new AuthenticationError('Invalid access token'); @@ -367,17 +381,18 @@ const startServer = async () => { scopes: result.rows[0].scopes.split(' '), accountId: result.rows[0].account_id, chosenLanguages: result.rows[0].chosen_languages, + permissions: result.rows[0].permissions, }; }; /** - * @param {any} req + * @param {Request} req * @returns {Promise} */ const accountFromRequest = (req) => new Promise((resolve, reject) => { const authorization = req.headers.authorization; - const location = url.parse(req.url, true); - const accessToken = location.query.access_token || req.headers['sec-websocket-protocol']; + const location = req.url ? url.parse(req.url, true) : undefined; + const accessToken = location?.query.access_token || req.headers['sec-websocket-protocol']; if (!authorization && !accessToken) { reject(new AuthenticationError('Missing access token')); @@ -386,11 +401,12 @@ const startServer = async () => { const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken; + // @ts-expect-error resolve(accountFromToken(token, req)); }); /** - * @param {any} req + * @param {Request} req * @returns {string|undefined} */ const channelNameFromPath = req => { @@ -422,7 +438,7 @@ const startServer = async () => { }; /** - * @param {http.IncomingMessage & ResolvedAccount} req + * @param {Request} req * @param {import('pino').Logger} logger * @param {string|undefined} channelName * @returns {Promise.} @@ -460,7 +476,7 @@ const startServer = async () => { */ /** - * @param {any} req + * @param {Request} req * @param {SystemMessageHandlers} eventHandlers * @returns {SubscriptionListener} */ @@ -485,7 +501,7 @@ const startServer = async () => { }; /** - * @param {http.IncomingMessage & ResolvedAccount} req + * @param {Request} req * @param {http.OutgoingMessage} res */ const subscribeHttpToSystemChannel = (req, res) => { @@ -512,8 +528,8 @@ const startServer = async () => { }; /** - * @param {any} req - * @param {any} res + * @param {Request} req + * @param {http.ServerResponse} res * @param {function(Error=): void} next */ const authenticationMiddleware = (req, res, next) => { @@ -542,8 +558,8 @@ const startServer = async () => { /** * @param {Error} err - * @param {any} req - * @param {any} res + * @param {Request} req + * @param {http.ServerResponse} res * @param {function(Error=): void} next */ const errorMiddleware = (err, req, res, next) => { @@ -561,16 +577,15 @@ const startServer = async () => { }; /** - * @param {any[]} arr + * @param {string[]} arr * @param {number=} shift * @returns {string} */ - // @ts-ignore const placeholders = (arr, shift = 0) => arr.map((_, i) => `$${i + 1 + shift}`).join(', '); /** * @param {string} listId - * @param {any} req + * @param {Request} req * @returns {Promise.} */ const authorizeListAccess = async (listId, req) => { @@ -583,18 +598,56 @@ const startServer = async () => { } }; + /** + * @param {string} kind + * @param {ResolvedAccount} account + * @returns {Promise.<{ localAccess: boolean, remoteAccess: boolean }>} + */ + const getFeedAccessSettings = async (kind, account) => { + const access = { localAccess: true, remoteAccess: true }; + + if (account.permissions & PERMISSION_VIEW_FEEDS) { + return access; + } + + let localAccessVar, remoteAccessVar; + + if (kind === 'hashtag') { + localAccessVar = 'local_topic_feed_access'; + remoteAccessVar = 'remote_topic_feed_access'; + } else { + localAccessVar = 'local_live_feed_access'; + remoteAccessVar = 'remote_live_feed_access'; + } + + const result = await pgPool.query('SELECT var, value FROM settings WHERE var IN ($1, $2)', [localAccessVar, remoteAccessVar]); + + result.rows.forEach((row) => { + if (row.var === localAccessVar) { + access.localAccess = row.value !== "--- disabled\n"; + } else { + access.remoteAccess = row.value !== "--- disabled\n"; + } + }); + + return access; + }; + /** * @param {string[]} channelIds - * @param {http.IncomingMessage & ResolvedAccount} req + * @param {Request} req * @param {import('pino').Logger} log * @param {function(string, string): void} output * @param {undefined | function(string[], SubscriptionListener): void} attachCloseHandler * @param {'websocket' | 'eventsource'} destinationType - * @param {boolean=} needsFiltering - * @param {boolean=} allowLocalOnly + * @param {Object} options + * @param {boolean} options.needsFiltering + * @param {boolean=} options.filterLocal + * @param {boolean=} options.filterRemote + * @param {boolean=} options.allowLocalOnly * @returns {SubscriptionListener} */ - const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, needsFiltering = false, allowLocalOnly = false) => { + const streamFrom = (channelIds, req, log, output, attachCloseHandler, destinationType, { needsFiltering, filterLocal, filterRemote, allowLocalOnly } = { needsFiltering: false, filterLocal: false, filterRemote: false, allowLocalOnly: false }) => { log.info({ channelIds }, `Starting stream`); /** @@ -641,6 +694,7 @@ const startServer = async () => { // The channels that need filtering are determined in the function // `channelNameToIds` defined below: if (!needsFiltering || (event !== 'update' && event !== 'status.update')) { + // @ts-expect-error transmit(event, payload); return; } @@ -648,8 +702,16 @@ const startServer = async () => { // The rest of the logic from here on in this function is to handle // filtering of statuses: + const localPayload = payload.account.username === payload.account.acct; + if (localPayload ? filterLocal : filterRemote) { + log.debug(`Message ${payload.id} filtered by feed settings`); + return; + } + // Filter based on language: + // @ts-expect-error if (Array.isArray(req.chosenLanguages) && req.chosenLanguages.indexOf(payload.language) === -1) { + // @ts-expect-error log.debug(`Message ${payload.id} filtered by language (${payload.language})`); return; } @@ -661,8 +723,9 @@ const startServer = async () => { } // Filter based on domain blocks, blocks, mutes, or custom filters: - // @ts-ignore + // @ts-expect-error const targetAccountIds = [payload.account.id].concat(payload.mentions.map(item => item.id)); + // @ts-expect-error const accountDomain = payload.account.acct.split('@')[1]; // TODO: Move this logic out of the message handling loop @@ -673,7 +736,7 @@ const startServer = async () => { } const queries = [ - // @ts-ignore + // @ts-expect-error client.query(`SELECT 1 FROM blocks WHERE (account_id = $1 AND target_account_id IN (${placeholders(targetAccountIds, 2)})) @@ -682,17 +745,19 @@ const startServer = async () => { SELECT 1 FROM mutes WHERE account_id = $1 - AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, payload.account.id].concat(targetAccountIds)), + AND target_account_id IN (${placeholders(targetAccountIds, 2)})`, [req.accountId, payload. + // @ts-expect-error + account.id].concat(targetAccountIds)), ]; if (accountDomain) { - // @ts-ignore + // @ts-expect-error queries.push(client.query('SELECT 1 FROM account_domain_blocks WHERE account_id = $1 AND domain = $2', [req.accountId, accountDomain])); } - // @ts-ignore + // @ts-expect-error if (!payload.filtered && !req.cachedFilters) { - // @ts-ignore + // @ts-expect-error queries.push(client.query('SELECT filter.id AS id, filter.phrase AS title, filter.context AS context, filter.expires_at AS expires_at, filter.action AS filter_action, keyword.keyword AS keyword, keyword.whole_word AS whole_word FROM custom_filter_keywords keyword JOIN custom_filters filter ON keyword.custom_filter_id = filter.id WHERE filter.account_id = $1 AND (filter.expires_at IS NULL OR filter.expires_at > NOW())', [req.accountId])); } @@ -701,6 +766,7 @@ const startServer = async () => { // Handling blocks & mutes and domain blocks: If one of those applies, // then we don't transmit the payload of the event to the client + // @ts-expect-error if (values[0].rows.length > 0 || (accountDomain && values[1].rows.length > 0)) { return; } @@ -717,9 +783,9 @@ const startServer = async () => { // TODO: Move this logic out of the message handling lifecycle // @ts-ignore if (!req.cachedFilters) { + // @ts-expect-error const filterRows = values[accountDomain ? 2 : 1].rows; - // @ts-ignore req.cachedFilters = filterRows.reduce((cache, filter) => { if (cache[filter.id]) { cache[filter.id].keywords.push([filter.keyword, filter.whole_word]); @@ -749,9 +815,9 @@ const startServer = async () => { // needs to be done in a separate loop as the database returns one // filterRow per keyword, so we need all the keywords before // constructing the regular expression - // @ts-ignore + // @ts-expect-error Object.keys(req.cachedFilters).forEach((key) => { - // @ts-ignore + // @ts-expect-error req.cachedFilters[key].regexp = new RegExp(req.cachedFilters[key].keywords.map(([keyword, whole_word]) => { let expr = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); @@ -772,16 +838,14 @@ const startServer = async () => { // Apply cachedFilters against the payload, constructing a // `filter_results` array of FilterResult entities - // @ts-ignore if (req.cachedFilters) { const status = payload; // TODO: Calculate searchableContent in Ruby on Rails: - // @ts-ignore + // @ts-expect-error const searchableContent = ([status.spoiler_text || '', status.content].concat((status.poll && status.poll.options) ? status.poll.options.map(option => option.title) : [])).concat(status.media_attachments.map(att => att.description)).join('\n\n').replace(//g, '\n').replace(/<\/p>

/g, '\n\n'); const searchableTextContent = JSDOM.fragment(searchableContent).textContent; const now = new Date(); - // @ts-ignore const filter_results = Object.values(req.cachedFilters).reduce((results, cachedFilter) => { // Check the filter hasn't expired before applying: if (cachedFilter.expires_at !== null && cachedFilter.expires_at < now) { @@ -841,8 +905,8 @@ const startServer = async () => { }; /** - * @param {any} req - * @param {any} res + * @param {Request} req + * @param {http.ServerResponse} res * @returns {function(string, string): void} */ const streamToHttp = (req, res) => { @@ -884,7 +948,7 @@ const startServer = async () => { }; /** - * @param {any} req + * @param {Request} req * @param {function(): void} [closeHandler] * @returns {function(string[], SubscriptionListener): void} */ @@ -934,10 +998,13 @@ const startServer = async () => { app.use(api); + // @ts-expect-error api.use(authenticationMiddleware); + // @ts-expect-error api.use(errorMiddleware); api.get('/api/v1/streaming/*', (req, res) => { + // @ts-expect-error const channelName = channelNameFromPath(req); // FIXME: In theory we'd never actually reach here due to @@ -948,12 +1015,15 @@ const startServer = async () => { return; } + // @ts-expect-error channelNameToIds(req, channelName, req.query).then(({ channelIds, options }) => { + // @ts-expect-error const onSend = streamToHttp(req, res); + // @ts-expect-error const onEnd = streamHttpEnd(req, subscriptionHeartbeat(channelIds)); // @ts-ignore - streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options.needsFiltering, options.allowLocalOnly); + streamFrom(channelIds, req, req.log, onSend, onEnd, 'eventsource', options); }).catch(err => { const {statusCode, errorMessage } = extractErrorStatusAndMessage(err); @@ -972,7 +1042,7 @@ const startServer = async () => { */ /** - * @param {any} req + * @param {Request} req * @returns {string[]} */ const channelsForUserStream = req => { @@ -986,12 +1056,28 @@ const startServer = async () => { }; /** - * @param {any} req + * @param {Request} req * @param {string} name * @param {StreamParams} params - * @returns {Promise.<{ channelIds: string[], options: { needsFiltering: boolean } }>} + * @returns {Promise.<{ channelIds: string[], options: { needsFiltering: boolean, filterLocal?: boolean, filterRemote?: boolean, allowLocalOnly?: boolean } }>} */ const channelNameToIds = (req, name, params) => new Promise((resolve, reject) => { + /** + * @param {string} feedKind + * @param {string} channelId + * @param {{ needsFiltering: boolean, allowLocalOnly: boolean }} options + */ + const resolveFeed = (feedKind, channelId, options) => { + getFeedAccessSettings(feedKind, req).then(({ localAccess, remoteAccess }) => { + resolve({ + channelIds: [channelId], + options: { ...options, filterLocal: !localAccess, filterRemote: !remoteAccess }, + }); + }).catch(() => { + reject(new Error('Error getting feed access settings')); + }); + }; + switch (name) { case 'user': resolve({ @@ -1008,60 +1094,28 @@ const startServer = async () => { break; case 'public': - resolve({ - channelIds: ['timeline:public'], - options: { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) }, - }); - + resolveFeed('public', 'timeline:public', { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) }); break; case 'public:allow_local_only': - resolve({ - channelIds: ['timeline:public'], - options: { needsFiltering: true, allowLocalOnly: true }, - }); - + resolveFeed('public', 'timeline:public', { needsFiltering: true, allowLocalOnly: true }); break; case 'public:local': - resolve({ - channelIds: ['timeline:public:local'], - options: { needsFiltering: true, allowLocalOnly: true }, - }); - + resolveFeed('public', 'timeline:public:local', { needsFiltering: true, allowLocalOnly: true }); break; case 'public:remote': - resolve({ - channelIds: ['timeline:public:remote'], - options: { needsFiltering: true, allowLocalOnly: false }, - }); - + resolveFeed('public', 'timeline:public:remote', { needsFiltering: true, allowLocalOnly: false }); break; case 'public:media': - resolve({ - channelIds: ['timeline:public:media'], - options: { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) }, - }); - + resolveFeed('public', 'timeline:public:media', { needsFiltering: true, allowLocalOnly: isTruthy(params.allow_local_only) }); break; case 'public:allow_local_only:media': - resolve({ - channelIds: ['timeline:public:media'], - options: { needsFiltering: true, allowLocalOnly: true }, - }); - + resolveFeed('public', 'timeline:public:media', { needsFiltering: true, allowLocalOnly: true }); break; case 'public:local:media': - resolve({ - channelIds: ['timeline:public:local:media'], - options: { needsFiltering: true, allowLocalOnly: true }, - }); - + resolveFeed('public', 'timeline:public:local:media', { needsFiltering: true, allowLocalOnly: true }); break; case 'public:remote:media': - resolve({ - channelIds: ['timeline:public:remote:media'], - options: { needsFiltering: true, allowLocalOnly: false }, - }); - + resolveFeed('public', 'timeline:public:remote:media', { needsFiltering: true, allowLocalOnly: false }); break; case 'direct': resolve({ @@ -1073,24 +1127,20 @@ const startServer = async () => { case 'hashtag': if (!params.tag) { reject(new RequestError('Missing tag name parameter')); - } else { - resolve({ - channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}`], - options: { needsFiltering: true, allowLocalOnly: true }, - }); + return; } + resolveFeed('hashtag', `timeline:hashtag:${normalizeHashtag(params.tag)}`, { needsFiltering: true, allowLocalOnly: true }); + break; case 'hashtag:local': if (!params.tag) { reject(new RequestError('Missing tag name parameter')); - } else { - resolve({ - channelIds: [`timeline:hashtag:${normalizeHashtag(params.tag)}:local`], - options: { needsFiltering: true, allowLocalOnly: true }, - }); + return; } + resolveFeed('hashtag', `timeline:hashtag:${normalizeHashtag(params.tag)}:local`, { needsFiltering: true, allowLocalOnly: true }); + break; case 'list': if (!params.list) { @@ -1131,7 +1181,7 @@ const startServer = async () => { /** * @typedef WebSocketSession * @property {import('ws').WebSocket & { isAlive: boolean}} websocket - * @property {http.IncomingMessage & ResolvedAccount} request + * @property {Request} request * @property {import('pino').Logger} logger * @property {Object.} subscriptions */ @@ -1153,7 +1203,7 @@ const startServer = async () => { const onSend = streamToWs(request, websocket, streamNameFromChannelName(channelName, params)); const stopHeartbeat = subscriptionHeartbeat(channelIds); - const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options.needsFiltering, options.allowLocalOnly); + const listener = streamFrom(channelIds, request, logger, onSend, undefined, 'websocket', options); metrics.connectedChannels.labels({ type: 'websocket', channel: channelName }).inc(); @@ -1257,7 +1307,7 @@ const startServer = async () => { /** * @param {import('ws').WebSocket & { isAlive: boolean }} ws - * @param {http.IncomingMessage & ResolvedAccount} req + * @param {Request} req * @param {import('pino').Logger} log */ function onConnection(ws, req, log) { @@ -1324,9 +1374,19 @@ const startServer = async () => { const { type, stream, ...params } = json; if (type === 'subscribe') { - subscribeWebsocketToChannel(session, firstParam(stream), params); + subscribeWebsocketToChannel( + session, + // @ts-expect-error + firstParam(stream), + params + ); } else if (type === 'unsubscribe') { - unsubscribeWebsocketFromChannel(session, firstParam(stream), params); + unsubscribeWebsocketFromChannel( + session, + // @ts-expect-error + firstParam(stream), + params + ); } else { // Unknown action type } @@ -1346,13 +1406,13 @@ const startServer = async () => { setInterval(() => { wss.clients.forEach(ws => { - // @ts-ignore + // @ts-expect-error if (ws.isAlive === false) { ws.terminate(); return; } - // @ts-ignore + // @ts-expect-error ws.isAlive = false; ws.ping('', false); }); @@ -1382,14 +1442,16 @@ const startServer = async () => { }; /** - * @param {any} server + * @param {http.Server} server * @param {function(string): void} [onSuccess] */ const attachServerWithConfig = (server, onSuccess) => { if (process.env.SOCKET) { server.listen(process.env.SOCKET, () => { if (onSuccess) { + // @ts-expect-error fs.chmodSync(server.address(), 0o666); + // @ts-expect-error onSuccess(server.address()); } }); @@ -1404,6 +1466,7 @@ const attachServerWithConfig = (server, onSuccess) => { server.listen(port, bind, () => { if (onSuccess) { + // @ts-expect-error onSuccess(`${server.address().address}:${server.address().port}`); } }); diff --git a/streaming/logging.js b/streaming/logging.js index e1c552c22e..61946b622c 100644 --- a/streaming/logging.js +++ b/streaming/logging.js @@ -100,7 +100,7 @@ export function createWebsocketLogger(request, resolvedAccount) { /** * Initializes the log level based on the environment - * @param {Object} env + * @param {Object} env * @param {string} environment */ export function initializeLogLevel(env, environment) { diff --git a/streaming/redis.js b/streaming/redis.js index 040246fda9..e8f28c0f90 100644 --- a/streaming/redis.js +++ b/streaming/redis.js @@ -6,6 +6,7 @@ import { parseIntFromEnvValue } from './utils.js'; * @typedef RedisConfiguration * @property {string|undefined} url * @property {import('ioredis').RedisOptions} options + * @property {string|undefined} namespace */ /** diff --git a/streaming/utils.js b/streaming/utils.js index 47c63dd4ca..dd5e82c67c 100644 --- a/streaming/utils.js +++ b/streaming/utils.js @@ -13,11 +13,15 @@ const FALSE_VALUES = [ ]; /** - * @param {any} value + * @typedef {typeof FALSE_VALUES[number]} FalseValue + */ + +/** + * @param {unknown} value * @returns {boolean} */ export function isTruthy(value) { - return value && !FALSE_VALUES.includes(value); + return !!value && !FALSE_VALUES.includes(/** @type {FalseValue} */ (value)); } /** diff --git a/vite.config.mts b/vite.config.mts index dee10a77bf..263b3d7959 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -73,7 +73,6 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { port: 3036, }, build: { - target: 'modules', commonjsOptions: { transformMixedEsModules: true }, chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB sourcemap: true, diff --git a/yarn.lock b/yarn.lock index 62b938b581..32be7911d0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2054,16 +2054,16 @@ __metadata: languageName: node linkType: hard -"@es-joy/jsdoccomment@npm:~0.52.0": - version: 0.52.0 - resolution: "@es-joy/jsdoccomment@npm:0.52.0" +"@es-joy/jsdoccomment@npm:~0.71.0": + version: 0.71.0 + resolution: "@es-joy/jsdoccomment@npm:0.71.0" dependencies: "@types/estree": "npm:^1.0.8" - "@typescript-eslint/types": "npm:^8.34.1" + "@typescript-eslint/types": "npm:^8.46.0" comment-parser: "npm:1.4.1" esquery: "npm:^1.6.0" - jsdoc-type-pratt-parser: "npm:~4.1.0" - checksum: 10c0/4def78060ef58859f31757b9d30c4939fc33e7d9ee85637a7f568c1d209c33aa0abd2cf5a3a4f3662ec5b12b85ecff2f2035d809dc93b9382a31a6dfb200d83c + jsdoc-type-pratt-parser: "npm:~6.6.0" + checksum: 10c0/fe64b729c18238c7e83f8fab30eab8ce97da6565adbb963011463f9abedef5393972ac1eeebd04b17b189e94bc389274dcb8f707023e96fd922d12dc608b5409 languageName: node linkType: hard @@ -2858,7 +2858,7 @@ __metadata: eslint-import-resolver-typescript: "npm:^4.2.5" eslint-plugin-formatjs: "npm:^5.3.1" eslint-plugin-import: "npm:~2.32.0" - eslint-plugin-jsdoc: "npm:^54.0.0" + eslint-plugin-jsdoc: "npm:^60.0.0" eslint-plugin-jsx-a11y: "npm:~6.10.2" eslint-plugin-promise: "npm:~7.2.1" eslint-plugin-react: "npm:^7.37.4" @@ -3368,8 +3368,8 @@ __metadata: linkType: hard "@reduxjs/toolkit@npm:^2.0.1": - version: 2.9.1 - resolution: "@reduxjs/toolkit@npm:2.9.1" + version: 2.9.2 + resolution: "@reduxjs/toolkit@npm:2.9.2" dependencies: "@standard-schema/spec": "npm:^1.0.0" "@standard-schema/utils": "npm:^0.3.0" @@ -3385,7 +3385,7 @@ __metadata: optional: true react-redux: optional: true - checksum: 10c0/11e99b665560c7e4bda80d26ad1308866282156bc177500558d72888d18819c303ebebf1f96121552facde3d6bd9c114b0e1f5c41e618c9ce0eaf464518f39dc + checksum: 10c0/577416200c76ffd82bce6158aaeb63e836ed2c2a14e670253056dcaec505da77643e79b47208b4e493a0c120a4a2bc049efe60cd555a2699053af5b03f2f2953 languageName: node linkType: hard @@ -3400,10 +3400,10 @@ __metadata: languageName: node linkType: hard -"@rolldown/pluginutils@npm:1.0.0-beta.38": - version: 1.0.0-beta.38 - resolution: "@rolldown/pluginutils@npm:1.0.0-beta.38" - checksum: 10c0/8353ec2528349f79e27d1a3193806725b85830da334e935cbb606d88c1177c58ea6519c578e4e93e5f677f5b22aecb8738894dbed14603e14b6bffe3facf1002 +"@rolldown/pluginutils@npm:1.0.0-beta.43": + version: 1.0.0-beta.43 + resolution: "@rolldown/pluginutils@npm:1.0.0-beta.43" + checksum: 10c0/1c17a0b16c277a0fdbab080fd22ef91e37c1f0d710ecfdacb6a080068062eb14ff030d0e9d2ec2325a1d4246dba0c49625755c82c0090f6cbf98d16e80183e02 languageName: node linkType: hard @@ -4659,13 +4659,20 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.34.1, @typescript-eslint/types@npm:^8.45.0": +"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.45.0": version: 8.45.0 resolution: "@typescript-eslint/types@npm:8.45.0" checksum: 10c0/0213a0573c671d13bc91961a2b2e814ec7f6381ff093bce6704017bd96b2fc7fee25906c815cedb32a0601cf5071ca6c7c5f940d087c3b0d3dd7d4bc03478278 languageName: node linkType: hard +"@typescript-eslint/types@npm:^8.46.0": + version: 8.46.1 + resolution: "@typescript-eslint/types@npm:8.46.1" + checksum: 10c0/90887acaa5b33b45af20cf7f87ec4ae098c0daa88484245473e73903fa6e542f613247c22148132167891ca06af6549a60b9d2fd14a65b22871e016901ce3756 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.45.0": version: 8.45.0 resolution: "@typescript-eslint/typescript-estree@npm:8.45.0" @@ -4888,18 +4895,18 @@ __metadata: linkType: hard "@vitejs/plugin-react@npm:^5.0.0": - version: 5.0.4 - resolution: "@vitejs/plugin-react@npm:5.0.4" + version: 5.1.0 + resolution: "@vitejs/plugin-react@npm:5.1.0" dependencies: "@babel/core": "npm:^7.28.4" "@babel/plugin-transform-react-jsx-self": "npm:^7.27.1" "@babel/plugin-transform-react-jsx-source": "npm:^7.27.1" - "@rolldown/pluginutils": "npm:1.0.0-beta.38" + "@rolldown/pluginutils": "npm:1.0.0-beta.43" "@types/babel__core": "npm:^7.20.5" - react-refresh: "npm:^0.17.0" + react-refresh: "npm:^0.18.0" peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - checksum: 10c0/bb9360a4b4c0abf064d22211756b999faf23889ac150de490590ca7bd029b0ef7f4cd8ba3a32b86682a62d46fb7bebd75b3fa9835c57c78123f4a646de2e0136 + checksum: 10c0/e192a12e2b854df109eafb1d06c0bc848e8e2b162c686aa6b999b1048658983e72674b2068ccc37562fcce44d32ad92b65f3a4e1897a0cb7859c2ee69cc63eac languageName: node linkType: hard @@ -5466,13 +5473,13 @@ __metadata: linkType: hard "axios@npm:^1.4.0": - version: 1.12.2 - resolution: "axios@npm:1.12.2" + version: 1.13.0 + resolution: "axios@npm:1.13.0" dependencies: follow-redirects: "npm:^1.15.6" form-data: "npm:^4.0.4" proxy-from-env: "npm:^1.1.0" - checksum: 10c0/80b063e318cf05cd33a4d991cea0162f3573481946f9129efb7766f38fde4c061c34f41a93a9f9521f02b7c9565ccbc197c099b0186543ac84a24580017adfed + checksum: 10c0/2af09f8ad9db9565bf97055eb0ddd2fd4abd9a03d23157b409348c9589370a88c3ede02e11fd1268becb780a77b62bdf9488650dd7208eda57edceca1d65622e languageName: node linkType: hard @@ -6377,7 +6384,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.1": +"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.6, debug@npm:^4.4.1, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -7144,23 +7151,25 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jsdoc@npm:^54.0.0": - version: 54.0.0 - resolution: "eslint-plugin-jsdoc@npm:54.0.0" +"eslint-plugin-jsdoc@npm:^60.0.0": + version: 60.8.3 + resolution: "eslint-plugin-jsdoc@npm:60.8.3" dependencies: - "@es-joy/jsdoccomment": "npm:~0.52.0" + "@es-joy/jsdoccomment": "npm:~0.71.0" are-docs-informative: "npm:^0.0.2" comment-parser: "npm:1.4.1" - debug: "npm:^4.4.1" + debug: "npm:^4.4.3" escape-string-regexp: "npm:^4.0.0" espree: "npm:^10.4.0" esquery: "npm:^1.6.0" + html-entities: "npm:^2.6.0" + object-deep-merge: "npm:^1.0.5" parse-imports-exports: "npm:^0.2.4" semver: "npm:^7.7.2" spdx-expression-parse: "npm:^4.0.0" peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - checksum: 10c0/cf0a388fc670ababe26f9584c467bc8c1592aa83affcf16118d8181c186a6d8f02a8ea65250766b45168fca5cb879a6af66e8457cdb98f0f923bd927572e2de5 + checksum: 10c0/2c5aa623a3e5f7410b36464df759ae5e7265ba6f9aaf67f7c16f9033c4a699532a3de702afe5bd6132717a61196be44aff170db36b71600278800770a9cd88ab languageName: node linkType: hard @@ -8175,6 +8184,13 @@ __metadata: languageName: node linkType: hard +"html-entities@npm:^2.6.0": + version: 2.6.0 + resolution: "html-entities@npm:2.6.0" + checksum: 10c0/7c8b15d9ea0cd00dc9279f61bab002ba6ca8a7a0f3c36ed2db3530a67a9621c017830d1d2c1c65beb9b8e3436ea663e9cf8b230472e0e413359399413b27c8b7 + languageName: node + linkType: hard + "html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" @@ -8924,10 +8940,10 @@ __metadata: languageName: node linkType: hard -"jsdoc-type-pratt-parser@npm:~4.1.0": - version: 4.1.0 - resolution: "jsdoc-type-pratt-parser@npm:4.1.0" - checksum: 10c0/7700372d2e733a32f7ea0a1df9cec6752321a5345c11a91b2ab478a031a426e934f16d5c1f15c8566c7b2c10af9f27892a29c2c789039f595470e929a4aa60ea +"jsdoc-type-pratt-parser@npm:~6.6.0": + version: 6.6.0 + resolution: "jsdoc-type-pratt-parser@npm:6.6.0" + checksum: 10c0/3cb9c28a945a66a925ebe40fd752113af01e655a0a0fedc6b1702e23c8f9ed187c45caf6cf94f009bde6cf5c98562524aa7a74ebb4571fca6d3ee5bef0344ec1 languageName: node linkType: hard @@ -9883,6 +9899,15 @@ __metadata: languageName: node linkType: hard +"object-deep-merge@npm:^1.0.5": + version: 1.0.5 + resolution: "object-deep-merge@npm:1.0.5" + dependencies: + type-fest: "npm:4.2.0" + checksum: 10c0/6664ecb43a2519c9b101f1c3b130dfc73e108d86ec06fbe7261505e1522cf8b69b10dd53b8cbb4cde35cca9d44d349667e2404f06fff85cf9f50b825bb6d1839 + languageName: node + linkType: hard + "object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" @@ -11318,10 +11343,10 @@ __metadata: languageName: node linkType: hard -"react-refresh@npm:^0.17.0": - version: 0.17.0 - resolution: "react-refresh@npm:0.17.0" - checksum: 10c0/002cba940384c9930008c0bce26cac97a9d5682bc623112c2268ba0c155127d9c178a9a5cc2212d560088d60dfd503edd808669a25f9b377f316a32361d0b23c +"react-refresh@npm:^0.18.0": + version: 0.18.0 + resolution: "react-refresh@npm:0.18.0" + checksum: 10c0/34a262f7fd803433a534f50deb27a148112a81adcae440c7d1cbae7ef14d21ea8f2b3d783e858cb7698968183b77755a38b4d4b5b1d79b4f4689c2f6d358fff2 languageName: node linkType: hard @@ -13403,6 +13428,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:4.2.0": + version: 4.2.0 + resolution: "type-fest@npm:4.2.0" + checksum: 10c0/75e0c112ae91d3b68c75da9b7563cf393f91ebdfca5d53d0b3f0405690217eadca318f9ddb89d58ee6ed67b8e32d23a4eae2aabc4e351e5ae184d610247bf772 + languageName: node + linkType: hard + "type-fest@npm:^0.16.0": version: 0.16.0 resolution: "type-fest@npm:0.16.0"