mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge pull request #3454 from glitch-soc/glitch-soc/merge-upstream
Merge upstream changes up to 0ef43a431d
This commit is contained in:
@@ -3,7 +3,18 @@ import { resolve } from 'node:path';
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../app/javascript/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
|
||||
stories: [
|
||||
{
|
||||
directory: '../app/javascript/mastodon',
|
||||
files: '**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
titlePrefix: 'Vanilla',
|
||||
},
|
||||
{
|
||||
directory: '../app/javascript/flavours/glitch',
|
||||
files: '**/*.stories.@(js|jsx|mjs|ts|tsx)',
|
||||
titlePrefix: 'Glitch',
|
||||
},
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-a11y',
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
|
||||
import { setProjectAnnotations } from '@storybook/react-vite';
|
||||
|
||||
import * as projectAnnotations from './preview';
|
||||
|
||||
// This is an important step to apply the right configuration when testing your stories.
|
||||
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
|
||||
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);
|
||||
@@ -68,6 +68,6 @@ The following table summarizes those limits.
|
||||
| Account `attributionDomains` | 256 | List will be truncated |
|
||||
| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated |
|
||||
| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected |
|
||||
| Media and avatar/header descriptions (`name`/`summary`) | 1500 | Description will be truncated |
|
||||
| Media and avatar/header descriptions (`name`/`summary`) | 10000 | Description will be truncated |
|
||||
| Collection name (`FeaturedCollection` `name`) | 256 | Name will be truncated |
|
||||
| Collection description (`FeaturedCollection` `summary`) | 2048 | Description will be truncated |
|
||||
|
||||
118
Gemfile.lock
118
Gemfile.lock
@@ -12,29 +12,29 @@ GEM
|
||||
specs:
|
||||
action_text-trix (2.1.17)
|
||||
railties
|
||||
actioncable (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actioncable (8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
zeitwerk (~> 2.6)
|
||||
actionmailbox (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionmailbox (8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
activejob (= 8.1.2.1)
|
||||
activerecord (= 8.1.2.1)
|
||||
activestorage (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
mail (>= 2.8.0)
|
||||
actionmailer (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionmailer (8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
actionview (= 8.1.2.1)
|
||||
activejob (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
mail (>= 2.8.0)
|
||||
rails-dom-testing (~> 2.2)
|
||||
actionpack (8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionpack (8.1.2.1)
|
||||
actionview (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
nokogiri (>= 1.8.5)
|
||||
rack (>= 2.2.4)
|
||||
rack-session (>= 1.0.1)
|
||||
@@ -42,16 +42,16 @@ GEM
|
||||
rails-dom-testing (~> 2.2)
|
||||
rails-html-sanitizer (~> 1.6)
|
||||
useragent (~> 0.16)
|
||||
actiontext (8.1.2)
|
||||
actiontext (8.1.2.1)
|
||||
action_text-trix (~> 2.1.15)
|
||||
actionpack (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionpack (= 8.1.2.1)
|
||||
activerecord (= 8.1.2.1)
|
||||
activestorage (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
globalid (>= 0.6.0)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
actionview (8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.11)
|
||||
rails-dom-testing (~> 2.2)
|
||||
@@ -61,22 +61,22 @@ GEM
|
||||
activemodel (>= 4.1)
|
||||
case_transform (>= 0.2)
|
||||
jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
|
||||
activejob (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activejob (8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activerecord (8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activemodel (8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
activerecord (8.1.2.1)
|
||||
activemodel (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
timeout (>= 0.4.0)
|
||||
activestorage (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
activestorage (8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
activejob (= 8.1.2.1)
|
||||
activerecord (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
marcel (~> 1.0)
|
||||
activesupport (8.1.2)
|
||||
activesupport (8.1.2.1)
|
||||
base64
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.3.1)
|
||||
@@ -251,12 +251,14 @@ GEM
|
||||
ffi-compiler (1.3.2)
|
||||
ffi (>= 1.15.5)
|
||||
rake
|
||||
flatware (2.3.4)
|
||||
flatware (2.4.0)
|
||||
benchmark
|
||||
drb
|
||||
logger
|
||||
thor (< 2.0)
|
||||
flatware-rspec (2.3.4)
|
||||
flatware (= 2.3.4)
|
||||
rspec (>= 3.6)
|
||||
flatware-rspec (2.4.0)
|
||||
flatware (= 2.4.0)
|
||||
rspec (>= 3.8)
|
||||
fog-core (2.6.0)
|
||||
builder
|
||||
excon (~> 1.0)
|
||||
@@ -470,7 +472,7 @@ GEM
|
||||
net-smtp (0.5.1)
|
||||
net-protocol
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.1)
|
||||
nokogiri (1.19.2)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
omniauth (2.1.4)
|
||||
@@ -655,20 +657,20 @@ GEM
|
||||
rack (>= 1.3)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
rails (8.1.2)
|
||||
actioncable (= 8.1.2)
|
||||
actionmailbox (= 8.1.2)
|
||||
actionmailer (= 8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
actiontext (= 8.1.2)
|
||||
actionview (= 8.1.2)
|
||||
activejob (= 8.1.2)
|
||||
activemodel (= 8.1.2)
|
||||
activerecord (= 8.1.2)
|
||||
activestorage (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
rails (8.1.2.1)
|
||||
actioncable (= 8.1.2.1)
|
||||
actionmailbox (= 8.1.2.1)
|
||||
actionmailer (= 8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
actiontext (= 8.1.2.1)
|
||||
actionview (= 8.1.2.1)
|
||||
activejob (= 8.1.2.1)
|
||||
activemodel (= 8.1.2.1)
|
||||
activerecord (= 8.1.2.1)
|
||||
activestorage (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
bundler (>= 1.15.0)
|
||||
railties (= 8.1.2)
|
||||
railties (= 8.1.2.1)
|
||||
rails-dom-testing (2.3.0)
|
||||
activesupport (>= 5.0.0)
|
||||
minitest
|
||||
@@ -679,9 +681,9 @@ GEM
|
||||
rails-i18n (8.1.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 8.0.0, < 9)
|
||||
railties (8.1.2)
|
||||
actionpack (= 8.1.2)
|
||||
activesupport (= 8.1.2)
|
||||
railties (8.1.2.1)
|
||||
actionpack (= 8.1.2.1)
|
||||
activesupport (= 8.1.2.1)
|
||||
irb (~> 1.13)
|
||||
rackup (>= 1.0.0)
|
||||
rake (>= 12.2)
|
||||
|
||||
@@ -62,7 +62,11 @@ module Admin
|
||||
end
|
||||
|
||||
def set_statuses
|
||||
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(:application, :preloadable_poll, :media_attachments, active_mentions: :account, reblog: [:account, :application, :preloadable_poll, :media_attachments, active_mentions: :account]).page(params[:page]).per(PER_PAGE)
|
||||
@statuses = Admin::StatusFilter.new(@account, filter_params).results.preload(*preload_columns, reblog: [:account, *preload_columns]).page(params[:page]).per(PER_PAGE)
|
||||
end
|
||||
|
||||
def preload_columns
|
||||
[:application, :preloadable_poll, :media_attachments, active_mentions: :account]
|
||||
end
|
||||
|
||||
def filter_params
|
||||
|
||||
@@ -26,6 +26,12 @@ module ContextHelper
|
||||
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
|
||||
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
|
||||
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
|
||||
profile_settings: {
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'showFeatured' => 'toot:showFeatured',
|
||||
'showMedia' => 'toot:showMedia',
|
||||
'showRepliesInMedia' => 'toot:showRepliesInMedia',
|
||||
},
|
||||
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
|
||||
quotes: {
|
||||
'quote' => 'https://w3id.org/fep/044f#quote',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { FC, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -19,7 +19,7 @@ import classes from './styles.module.css';
|
||||
export interface CalloutProps {
|
||||
variant?:
|
||||
| 'default'
|
||||
// | 'subtle'
|
||||
| 'subtle'
|
||||
| 'feature'
|
||||
| 'inverted'
|
||||
| 'success'
|
||||
@@ -31,9 +31,9 @@ export interface CalloutProps {
|
||||
/** Set to false to hide the icon. */
|
||||
icon?: IconProp | boolean;
|
||||
onPrimary?: () => void;
|
||||
primaryLabel?: string;
|
||||
primaryLabel?: string | ReactElement;
|
||||
onSecondary?: () => void;
|
||||
secondaryLabel?: string;
|
||||
secondaryLabel?: string | ReactElement;
|
||||
onClose?: () => void;
|
||||
id?: string;
|
||||
extraContent?: ReactNode;
|
||||
@@ -41,7 +41,7 @@ export interface CalloutProps {
|
||||
|
||||
const variantClasses = {
|
||||
default: classes.variantDefault as string,
|
||||
// subtle: classes.variantSubtle as string,
|
||||
subtle: classes.variantSubtle as string,
|
||||
feature: classes.variantFeature as string,
|
||||
inverted: classes.variantInverted as string,
|
||||
success: classes.variantSuccess as string,
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
@@ -51,6 +55,7 @@
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
text-wrap: nowrap;
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease-in-out;
|
||||
|
||||
@@ -80,14 +85,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* .variantSubtle {
|
||||
.variantSubtle {
|
||||
border: 1px solid var(--color-bg-brand-softer);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
.variantFeature {
|
||||
background-color: var(--color-bg-brand-base);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Combobox } from '@/flavours/glitch/components/form_fields';
|
||||
import { useSearchTags } from '@/flavours/glitch/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/flavours/glitch/hooks/useSearchTags';
|
||||
import { addFeaturedTag } from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||
import { addFeaturedTags } from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||
import { useAppDispatch } from '@/flavours/glitch/store';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
|
||||
@@ -47,7 +47,7 @@ export const AccountEditTagSearch: FC = () => {
|
||||
(item: TagSearchResult) => {
|
||||
resetSearch();
|
||||
setQuery('');
|
||||
void dispatch(addFeaturedTag({ name: item.name }));
|
||||
void dispatch(addFeaturedTags({ names: [item.name] }));
|
||||
},
|
||||
[dispatch, resetSearch],
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||
import type { TagData } from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||
import {
|
||||
addFeaturedTag,
|
||||
addFeaturedTags,
|
||||
deleteFeaturedTag,
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
@@ -128,7 +128,7 @@ const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleAddTag = useCallback(() => {
|
||||
void dispatch(addFeaturedTag({ name }));
|
||||
void dispatch(addFeaturedTags({ names: [name] }));
|
||||
}, [dispatch, name]);
|
||||
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,10 @@ import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator
|
||||
import { RemoteHint } from '@/flavours/glitch/components/remote_hint';
|
||||
import StatusList from '@/flavours/glitch/components/status_list';
|
||||
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
|
||||
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||
import {
|
||||
useAccountId,
|
||||
useCurrentAccountId,
|
||||
} from '@/flavours/glitch/hooks/useAccountId';
|
||||
import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility';
|
||||
import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
|
||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||
@@ -34,6 +37,7 @@ import {
|
||||
usePinnedStatusIds,
|
||||
} from './pinned_statuses';
|
||||
import classes from './styles.module.scss';
|
||||
import { TagSuggestions } from './tags_suggestions';
|
||||
|
||||
const emptyList = ImmutableList<string>();
|
||||
|
||||
@@ -135,6 +139,7 @@ const Prepend: FC<{
|
||||
accountId: string;
|
||||
forceEmpty: boolean;
|
||||
}> = ({ forceEmpty, accountId }) => {
|
||||
const me = useCurrentAccountId();
|
||||
if (forceEmpty) {
|
||||
return <AccountHeader accountId={accountId} hideTabs />;
|
||||
}
|
||||
@@ -144,6 +149,7 @@ const Prepend: FC<{
|
||||
<AccountHeader accountId={accountId} hideTabs />
|
||||
<AccountFilters />
|
||||
<FeaturedTags accountId={accountId} />
|
||||
{me === accountId && <TagSuggestions />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,8 +49,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
.tagsWrapper,
|
||||
.tagSuggestions {
|
||||
margin: 0 24px 8px;
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedList } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchFeaturedTags } from '@/flavours/glitch/actions/featured_tags';
|
||||
import { Callout } from '@/flavours/glitch/components/callout';
|
||||
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||
import { useDismissible } from '@/flavours/glitch/hooks/useDismissible';
|
||||
import {
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
addFeaturedTags,
|
||||
} from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const MAX_SUGGESTED_TAGS = 3;
|
||||
|
||||
export const TagSuggestions: FC = () => {
|
||||
const { dismiss, wasDismissed } = useDismissible(
|
||||
'profile/featured_tag_suggestions',
|
||||
);
|
||||
|
||||
const suggestedTags = useAppSelector((state) =>
|
||||
state.profileEdit.tagSuggestions?.slice(0, MAX_SUGGESTED_TAGS),
|
||||
);
|
||||
const existingTagCount = useAppSelector(
|
||||
(state) => state.profileEdit.profile?.featuredTags.length,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = !suggestedTags || existingTagCount === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
void dispatch(fetchProfile());
|
||||
void dispatch(fetchSuggestedTags());
|
||||
}
|
||||
}, [dispatch, isLoading]);
|
||||
|
||||
const me = useCurrentAccountId();
|
||||
const [showSuccessNotice, setSuccessNotice] = useState(false);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
if (!suggestedTags?.length || !me) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addTags = async () => {
|
||||
await dispatch(
|
||||
addFeaturedTags({ names: suggestedTags.map((tag) => tag.name) }),
|
||||
);
|
||||
await dispatch(fetchFeaturedTags({ accountId: me }));
|
||||
setSuccessNotice(true);
|
||||
dismiss();
|
||||
};
|
||||
void addTags();
|
||||
}, [dismiss, dispatch, me, suggestedTags]);
|
||||
|
||||
const handleDismissSuccessNotice = useCallback(() => {
|
||||
setSuccessNotice(false);
|
||||
}, []);
|
||||
|
||||
if (showSuccessNotice) {
|
||||
return (
|
||||
<Callout
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onClose={handleDismissSuccessNotice}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.added'
|
||||
defaultMessage='Manage your featured hashtags at any time under <link>Edit Profile > Featured hashtags</link>.'
|
||||
values={{
|
||||
link: (chunks) => <Link to='/profile/featured_tags'>{chunks}</Link>,
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isLoading ||
|
||||
!suggestedTags.length ||
|
||||
existingTagCount > 0 ||
|
||||
wasDismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout
|
||||
id='featured_tags.suggestions'
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onPrimary={handleAdd}
|
||||
primaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
}
|
||||
onSecondary={dismiss}
|
||||
secondaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.dismiss'
|
||||
defaultMessage='No thanks'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions'
|
||||
defaultMessage='Lately you’ve posted about {items}. Add these as featured hashtags?'
|
||||
values={{
|
||||
items: (
|
||||
<FormattedList
|
||||
value={suggestedTags.map(({ name }) => `#${name}`)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants';
|
||||
import {
|
||||
importCustomEmojiData,
|
||||
importEmojiData,
|
||||
importLegacyShortcodes,
|
||||
} from './loader';
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
self.postMessage('ready'); // After the worker is ready, notify the main thread
|
||||
@@ -16,6 +11,8 @@ function handleMessage(event: MessageEvent<{ locale: string }>) {
|
||||
}
|
||||
|
||||
async function loadData(locale: string) {
|
||||
const { importCustomEmojiData, importEmojiData, importLegacyShortcodes } =
|
||||
await import('./loader');
|
||||
let importCount: number | undefined;
|
||||
if (locale === EMOJI_TYPE_CUSTOM) {
|
||||
importCount = (await importCustomEmojiData())?.length;
|
||||
|
||||
@@ -104,24 +104,24 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||
builder.addCase(addFeaturedTags.pending, (state) => {
|
||||
state.isPending = true;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.rejected, (state) => {
|
||||
builder.addCase(addFeaturedTags.rejected, (state) => {
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
||||
builder.addCase(addFeaturedTags.fulfilled, (state, action) => {
|
||||
if (!state.profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.profile.featuredTags = [
|
||||
...state.profile.featuredTags,
|
||||
transformTag(action.payload),
|
||||
...action.payload.map(transformTag),
|
||||
].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
if (state.tagSuggestions) {
|
||||
state.tagSuggestions = state.tagSuggestions.filter(
|
||||
(tag) => tag.name !== action.meta.arg.name,
|
||||
(tag) => !action.meta.arg.names.includes(tag.name),
|
||||
);
|
||||
}
|
||||
state.isPending = false;
|
||||
@@ -350,20 +350,11 @@ export const fetchSuggestedTags = createDataLoadingThunk(
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const addFeaturedTag = createDataLoadingThunk(
|
||||
export const addFeaturedTags = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/addFeaturedTag`,
|
||||
({ name }: { name: string }) => apiPostFeaturedTag(name),
|
||||
{
|
||||
condition(arg, { getState }) {
|
||||
const state = getState();
|
||||
return (
|
||||
!!state.profileEdit.profile &&
|
||||
!state.profileEdit.profile.featuredTags.some(
|
||||
(tag) => tag.name === arg.name,
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
({ names }: { names: string[] }) =>
|
||||
Promise.all(names.map((n) => apiPostFeaturedTag(n))),
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const deleteFeaturedTag = createDataLoadingThunk(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { FC, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -19,7 +19,7 @@ import classes from './styles.module.css';
|
||||
export interface CalloutProps {
|
||||
variant?:
|
||||
| 'default'
|
||||
// | 'subtle'
|
||||
| 'subtle'
|
||||
| 'feature'
|
||||
| 'inverted'
|
||||
| 'success'
|
||||
@@ -31,9 +31,9 @@ export interface CalloutProps {
|
||||
/** Set to false to hide the icon. */
|
||||
icon?: IconProp | boolean;
|
||||
onPrimary?: () => void;
|
||||
primaryLabel?: string;
|
||||
primaryLabel?: string | ReactElement;
|
||||
onSecondary?: () => void;
|
||||
secondaryLabel?: string;
|
||||
secondaryLabel?: string | ReactElement;
|
||||
onClose?: () => void;
|
||||
id?: string;
|
||||
extraContent?: ReactNode;
|
||||
@@ -41,7 +41,7 @@ export interface CalloutProps {
|
||||
|
||||
const variantClasses = {
|
||||
default: classes.variantDefault as string,
|
||||
// subtle: classes.variantSubtle as string,
|
||||
subtle: classes.variantSubtle as string,
|
||||
feature: classes.variantFeature as string,
|
||||
inverted: classes.variantInverted as string,
|
||||
success: classes.variantSuccess as string,
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
@@ -51,6 +55,7 @@
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
text-wrap: nowrap;
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease-in-out;
|
||||
|
||||
@@ -80,14 +85,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* .variantSubtle {
|
||||
.variantSubtle {
|
||||
border: 1px solid var(--color-bg-brand-softer);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
.variantFeature {
|
||||
background-color: var(--color-bg-brand-base);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Combobox } from '@/mastodon/components/form_fields';
|
||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||
import { addFeaturedTag } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
|
||||
@@ -47,7 +47,7 @@ export const AccountEditTagSearch: FC = () => {
|
||||
(item: TagSearchResult) => {
|
||||
resetSearch();
|
||||
setQuery('');
|
||||
void dispatch(addFeaturedTag({ name: item.name }));
|
||||
void dispatch(addFeaturedTags({ names: [item.name] }));
|
||||
},
|
||||
[dispatch, resetSearch],
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import type { TagData } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import {
|
||||
addFeaturedTag,
|
||||
addFeaturedTags,
|
||||
deleteFeaturedTag,
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
@@ -128,7 +128,7 @@ const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleAddTag = useCallback(() => {
|
||||
void dispatch(addFeaturedTag({ name }));
|
||||
void dispatch(addFeaturedTags({ names: [name] }));
|
||||
}, [dispatch, name]);
|
||||
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,10 @@ import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { RemoteHint } from '@/mastodon/components/remote_hint';
|
||||
import StatusList from '@/mastodon/components/status_list';
|
||||
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import {
|
||||
useAccountId,
|
||||
useCurrentAccountId,
|
||||
} from '@/mastodon/hooks/useAccountId';
|
||||
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||
import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
@@ -34,6 +37,7 @@ import {
|
||||
usePinnedStatusIds,
|
||||
} from './pinned_statuses';
|
||||
import classes from './styles.module.scss';
|
||||
import { TagSuggestions } from './tags_suggestions';
|
||||
|
||||
const emptyList = ImmutableList<string>();
|
||||
|
||||
@@ -135,6 +139,7 @@ const Prepend: FC<{
|
||||
accountId: string;
|
||||
forceEmpty: boolean;
|
||||
}> = ({ forceEmpty, accountId }) => {
|
||||
const me = useCurrentAccountId();
|
||||
if (forceEmpty) {
|
||||
return <AccountHeader accountId={accountId} hideTabs />;
|
||||
}
|
||||
@@ -144,6 +149,7 @@ const Prepend: FC<{
|
||||
<AccountHeader accountId={accountId} hideTabs />
|
||||
<AccountFilters />
|
||||
<FeaturedTags accountId={accountId} />
|
||||
{me === accountId && <TagSuggestions />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,8 +49,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
.tagsWrapper,
|
||||
.tagSuggestions {
|
||||
margin: 0 24px 8px;
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedList } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchFeaturedTags } from '@/mastodon/actions/featured_tags';
|
||||
import { Callout } from '@/mastodon/components/callout';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||
import {
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
addFeaturedTags,
|
||||
} from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const MAX_SUGGESTED_TAGS = 3;
|
||||
|
||||
export const TagSuggestions: FC = () => {
|
||||
const { dismiss, wasDismissed } = useDismissible(
|
||||
'profile/featured_tag_suggestions',
|
||||
);
|
||||
|
||||
const suggestedTags = useAppSelector((state) =>
|
||||
state.profileEdit.tagSuggestions?.slice(0, MAX_SUGGESTED_TAGS),
|
||||
);
|
||||
const existingTagCount = useAppSelector(
|
||||
(state) => state.profileEdit.profile?.featuredTags.length,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = !suggestedTags || existingTagCount === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
void dispatch(fetchProfile());
|
||||
void dispatch(fetchSuggestedTags());
|
||||
}
|
||||
}, [dispatch, isLoading]);
|
||||
|
||||
const me = useCurrentAccountId();
|
||||
const [showSuccessNotice, setSuccessNotice] = useState(false);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
if (!suggestedTags?.length || !me) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addTags = async () => {
|
||||
await dispatch(
|
||||
addFeaturedTags({ names: suggestedTags.map((tag) => tag.name) }),
|
||||
);
|
||||
await dispatch(fetchFeaturedTags({ accountId: me }));
|
||||
setSuccessNotice(true);
|
||||
dismiss();
|
||||
};
|
||||
void addTags();
|
||||
}, [dismiss, dispatch, me, suggestedTags]);
|
||||
|
||||
const handleDismissSuccessNotice = useCallback(() => {
|
||||
setSuccessNotice(false);
|
||||
}, []);
|
||||
|
||||
if (showSuccessNotice) {
|
||||
return (
|
||||
<Callout
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onClose={handleDismissSuccessNotice}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.added'
|
||||
defaultMessage='Manage your featured hashtags at any time under <link>Edit Profile > Featured hashtags</link>.'
|
||||
values={{
|
||||
link: (chunks) => <Link to='/profile/featured_tags'>{chunks}</Link>,
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isLoading ||
|
||||
!suggestedTags.length ||
|
||||
existingTagCount > 0 ||
|
||||
wasDismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout
|
||||
id='featured_tags.suggestions'
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onPrimary={handleAdd}
|
||||
primaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
}
|
||||
onSecondary={dismiss}
|
||||
secondaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.dismiss'
|
||||
defaultMessage='No thanks'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions'
|
||||
defaultMessage='Lately you’ve posted about {items}. Add these as featured hashtags?'
|
||||
values={{
|
||||
items: (
|
||||
<FormattedList
|
||||
value={suggestedTags.map(({ name }) => `#${name}`)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,4 @@
|
||||
import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants';
|
||||
import {
|
||||
importCustomEmojiData,
|
||||
importEmojiData,
|
||||
importLegacyShortcodes,
|
||||
} from './loader';
|
||||
|
||||
addEventListener('message', handleMessage);
|
||||
self.postMessage('ready'); // After the worker is ready, notify the main thread
|
||||
@@ -16,6 +11,8 @@ function handleMessage(event: MessageEvent<{ locale: string }>) {
|
||||
}
|
||||
|
||||
async function loadData(locale: string) {
|
||||
const { importCustomEmojiData, importEmojiData, importLegacyShortcodes } =
|
||||
await import('./loader');
|
||||
let importCount: number | undefined;
|
||||
if (locale === EMOJI_TYPE_CUSTOM) {
|
||||
importCount = (await importCustomEmojiData())?.length;
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Дадайце да {count} уліковых запісаў, на якія Вы падпісаныя",
|
||||
"collections.accounts.empty_title": "Гэтая калекцыя пустая",
|
||||
"collections.collection_description": "Апісанне",
|
||||
"collections.collection_language": "Мова",
|
||||
"collections.collection_language_none": "Няма",
|
||||
"collections.collection_name": "Назва",
|
||||
"collections.collection_topic": "Тэма",
|
||||
"collections.confirm_account_removal": "Упэўненыя, што хочаце прыбраць гэты ўліковы запіс з гэтай калекцыі?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural,one {Замацаваны допіс} other {Замацаваныя допісы}}",
|
||||
"featured_carousel.slide": "Допіс {current, number} з {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Апошнім часам Вы рабілі допісы пра {items}. Дадаць гэта ў рэкамендаваныя хэштэгі?",
|
||||
"featured_tags.suggestions.add": "Дадаць",
|
||||
"featured_tags.suggestions.added": "Кіруйце сваімі рэкамендаванымі хэштэгамі ў любы час праз <link>Рэдагаваць профіль > Рэкамендаваныя хэштэгі</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Не, дзякуй",
|
||||
"filter_modal.added.context_mismatch_explanation": "Гэтая катэгорыя фільтра не прымяняецца да кантэксту, у якім Вы адкрылі гэты допіс. Калі Вы хочаце, каб паведамленне таксама было адфільтраванае ў гэтым кантэксце, Вам трэба будзе адрэдагаваць фільтр.",
|
||||
"filter_modal.added.context_mismatch_title": "Неадпаведны кантэкст!",
|
||||
"filter_modal.added.expired_explanation": "Тэрмін дзеяння гэтай катэгорыі фільтраў скончыўся, вам трэба будзе змяніць дату заканчэння тэрміну дзеяння, каб яна прымянялася",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Tilføj op til {count} konti, du følger",
|
||||
"collections.accounts.empty_title": "Denne samling er tom",
|
||||
"collections.collection_description": "Beskrivelse",
|
||||
"collections.collection_language": "Sprog",
|
||||
"collections.collection_language_none": "Intet",
|
||||
"collections.collection_name": "Navn",
|
||||
"collections.collection_topic": "Emne",
|
||||
"collections.confirm_account_removal": "Er du sikker på, at du vil fjerne denne konto fra denne samling?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {fastgjort indlæg} other {fastgjorte indlæg}}",
|
||||
"featured_carousel.slide": "Indlæg {current, number} af {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Du har for nylig lagt indlæg op om {items}. Skal disse tilføjes som fremhævede hashtags?",
|
||||
"featured_tags.suggestions.add": "Tilføj",
|
||||
"featured_tags.suggestions.added": "Du kan til enhver tid administrere dine fremhævede hashtags under <link>Rediger profil > Fremhævede hashtags</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Nej tak",
|
||||
"filter_modal.added.context_mismatch_explanation": "Denne filterkategori omfatter ikke konteksten, hvorunder dette indlæg er tilgået. Redigér filteret, hvis indlægget også ønskes filtreret i denne kontekst.",
|
||||
"filter_modal.added.context_mismatch_title": "Kontekstmisforhold!",
|
||||
"filter_modal.added.expired_explanation": "Denne filterkategori er udløbet. Ændr dens udløbsdato, for at anvende den.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Füge bis zu {count} Konten, denen du folgst, hinzu",
|
||||
"collections.accounts.empty_title": "Diese Sammlung ist leer",
|
||||
"collections.collection_description": "Beschreibung",
|
||||
"collections.collection_language": "Sprache",
|
||||
"collections.collection_language_none": "Nicht festgelegt",
|
||||
"collections.collection_name": "Titel",
|
||||
"collections.collection_topic": "Thema",
|
||||
"collections.confirm_account_removal": "Möchtest du dieses Konto wirklich aus der Sammlung entfernen?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Angehefteter Beitrag} other {Angeheftete Beiträge}}",
|
||||
"featured_carousel.slide": "Beitrag {current, number} von {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "In deinen letzten Beiträgen hast du {items} verwendet. Möchtest du sie zu deinen vorgestellten Hashtags hinzufügen?",
|
||||
"featured_tags.suggestions.add": "Hinzufügen",
|
||||
"featured_tags.suggestions.added": "Du kannst deine vorgestellten Hashtags jederzeit in den Einstellungen <link>Profil bearbeiten > Vorgestellte Hashtags</link> anpassen.",
|
||||
"featured_tags.suggestions.dismiss": "Nein danke",
|
||||
"filter_modal.added.context_mismatch_explanation": "Diese Filterkategorie gilt nicht für den Kontext, in welchem du auf diesen Beitrag zugegriffen hast. Wenn der Beitrag auch in diesem Kontext gefiltert werden soll, musst du den Filter bearbeiten.",
|
||||
"filter_modal.added.context_mismatch_title": "Kontext stimmt nicht überein!",
|
||||
"filter_modal.added.expired_explanation": "Diese Filterkategorie ist abgelaufen. Du musst das Ablaufdatum für diese Kategorie ändern.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Προσθέστε μέχρι και {count} λογαριασμούς που ακολουθείτε",
|
||||
"collections.accounts.empty_title": "Αυτή η συλλογή είναι κενή",
|
||||
"collections.collection_description": "Περιγραφή",
|
||||
"collections.collection_language": "Γλώσσα",
|
||||
"collections.collection_language_none": "Καμία",
|
||||
"collections.collection_name": "Όνομα",
|
||||
"collections.collection_topic": "Θέμα",
|
||||
"collections.confirm_account_removal": "Σίγουρα θέλετε να αφαιρέσετε αυτόν τον λογαριασμό από αυτή τη συλλογή;",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Καρφιτσωμένη Ανάρτηση} other {Καρφιτσωμένες Αναρτήσεις}}",
|
||||
"featured_carousel.slide": "Ανάρτηση {current, number} από {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Τον τελευταίο καιρό έχετε δημοσιεύσει για {items}. Προσθέστε αυτά ως αναδεδειγμένες ετικέτες;",
|
||||
"featured_tags.suggestions.add": "Προσθήκη",
|
||||
"featured_tags.suggestions.added": "Διαχειριστείτε τις αναδεδειγμένες ετικέτες σας οποιαδήποτε στιγμή κάτω από το <link>Επεξεργασία προφίλ > Αναδεδειγμένες ετικέτες</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Όχι, ευχαριστώ",
|
||||
"filter_modal.added.context_mismatch_explanation": "Αυτή η κατηγορία φίλτρων δεν ισχύει για το περιεχόμενο εντός του οποίου προσπελάσατε αυτή την ανάρτηση. Αν θέλετε να φιλτραριστεί η ανάρτηση και εντός αυτού του πλαισίου, θα πρέπει να τροποποιήσετε το φίλτρο.",
|
||||
"filter_modal.added.context_mismatch_title": "Ασυμφωνία περιεχομένου!",
|
||||
"filter_modal.added.expired_explanation": "Αυτή η κατηγορία φίλτρων έχει λήξει, πρέπει να αλλάξετε την ημερομηνία λήξης για να ισχύσει.",
|
||||
|
||||
@@ -628,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
|
||||
"featured_carousel.slide": "Post {current, number} of {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Lately you’ve posted about {items}. Add these as featured hashtags?",
|
||||
"featured_tags.suggestions.add": "Add",
|
||||
"featured_tags.suggestions.added": "Manage your featured hashtags at any time under <link>Edit Profile > Featured hashtags</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No thanks",
|
||||
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
||||
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
||||
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Agregá hasta {count} cuentas que seguís",
|
||||
"collections.accounts.empty_title": "Esta colección está vacía",
|
||||
"collections.collection_description": "Descripción",
|
||||
"collections.collection_language": "Idioma",
|
||||
"collections.collection_language_none": "Ninguno",
|
||||
"collections.collection_name": "Nombre",
|
||||
"collections.collection_topic": "Tema",
|
||||
"collections.confirm_account_removal": "¿De verdad querés eliminar esta cuenta de esta colección?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Mensaje fijado} other {Mensajes fijados}}",
|
||||
"featured_carousel.slide": "Mensaje {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Recientemente, publicaste sobre {items}. ¿Querés agregarlas como etiquetas destacadas?",
|
||||
"featured_tags.suggestions.add": "Agregar",
|
||||
"featured_tags.suggestions.added": "Administrá tus etiquetas destacadas cuando quieras en <link>Editar perfil > Etiquetas destacadas</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No, gracias",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que accediste a este mensaje. Si querés que el mensaje sea filtrado también en este contexto, vas a tener que editar el filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "¡El contexto no coincide!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoría de filtro caducó; vas a necesitar cambiar la fecha de caducidad para que se aplique.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Añade hasta {count} cuentas que sigues",
|
||||
"collections.accounts.empty_title": "Esta colección está vacía",
|
||||
"collections.collection_description": "Descripción",
|
||||
"collections.collection_language": "Idioma",
|
||||
"collections.collection_language_none": "Ninguno",
|
||||
"collections.collection_name": "Nombre",
|
||||
"collections.collection_topic": "Tema",
|
||||
"collections.confirm_account_removal": "¿Estás seguro/a de que quieres eliminar esta cuenta de esta colección?",
|
||||
@@ -466,7 +468,7 @@
|
||||
"confirmation_modal.cancel": "Cancelar",
|
||||
"confirmations.block.confirm": "Bloquear",
|
||||
"confirmations.delete.confirm": "Eliminar",
|
||||
"confirmations.delete.message": "¿Estás seguro de que quieres borrar esta publicación?",
|
||||
"confirmations.delete.message": "¿Estás seguro de que quieres eliminar esta publicación?",
|
||||
"confirmations.delete.title": "¿Deseas eliminar la publicación?",
|
||||
"confirmations.delete_collection.confirm": "Eliminar",
|
||||
"confirmations.delete_collection.message": "Esta acción no se puede deshacer.",
|
||||
@@ -506,9 +508,9 @@
|
||||
"confirmations.quiet_post_quote_info.got_it": "Entendido",
|
||||
"confirmations.quiet_post_quote_info.message": "Al citar una publicación pública discreta, tu publicación se ocultará de las cronologías de tendencias.",
|
||||
"confirmations.quiet_post_quote_info.title": "Citar publicaciones públicas discretas",
|
||||
"confirmations.redraft.confirm": "Borrar y volver a borrador",
|
||||
"confirmations.redraft.message": "¿Estás seguro de que quieres borrar esta publicación y editarla? Los favoritos e impulsos se perderán, y las respuestas a la publicación original quedarán separadas.",
|
||||
"confirmations.redraft.title": "¿Deseas borrar y volver a redactar la publicación?",
|
||||
"confirmations.redraft.confirm": "Eliminar y volver a redactar",
|
||||
"confirmations.redraft.message": "¿Estás seguro de que quieres eliminar esta publicación y volver a redactarla? Se perderán tanto los «Me gusta» como los impulsos, y las respuestas a la publicación original quedarán sin referencia.",
|
||||
"confirmations.redraft.title": "¿Deseas eliminar y volver a redactar la publicación?",
|
||||
"confirmations.remove_from_followers.confirm": "Eliminar seguidor",
|
||||
"confirmations.remove_from_followers.message": "{name} dejará de seguirte. ¿Estás seguro de que quieres continuar?",
|
||||
"confirmations.remove_from_followers.title": "¿Eliminar seguidor?",
|
||||
@@ -527,7 +529,7 @@
|
||||
"content_warning.hide": "Ocultar publicación",
|
||||
"content_warning.show": "Mostrar de todos modos",
|
||||
"content_warning.show_more": "Mostrar más",
|
||||
"conversation.delete": "Borrar conversación",
|
||||
"conversation.delete": "Eliminar conversación",
|
||||
"conversation.mark_as_read": "Marcar como leído",
|
||||
"conversation.open": "Ver conversación",
|
||||
"conversation.with": "Con {names}",
|
||||
@@ -570,7 +572,7 @@
|
||||
"embed.instructions": "Añade esta publicación a tu sitio web con el siguiente código.",
|
||||
"embed.preview": "Así es como se verá:",
|
||||
"emoji_button.activity": "Actividad",
|
||||
"emoji_button.clear": "Borrar",
|
||||
"emoji_button.clear": "Eliminar",
|
||||
"emoji_button.custom": "Personalizado",
|
||||
"emoji_button.flags": "Marcas",
|
||||
"emoji_button.food": "Comida y bebida",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural,one {Publicación fijada}other {Publicaciones fijadas}}",
|
||||
"featured_carousel.slide": "Publicación {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Últimamente has publicado sobre {items}. ¿Deseas añadirlo como etiqueta destacada?",
|
||||
"featured_tags.suggestions.add": "Añadir",
|
||||
"featured_tags.suggestions.added": "Controla tus etiquetas destacadas en cualquier momento en <link>Editar perfil > Etiquetas destacadas</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No, gracias",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que has accedido a esta publicación. Si deseas que la publicación también se filtre en este contexto, tendrás que editar el filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "¡El contexto no coincide!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado; deberás cambiar la fecha de caducidad para que se aplique.",
|
||||
@@ -798,7 +804,7 @@
|
||||
"lists.create": "Crear",
|
||||
"lists.create_a_list_to_organize": "Crea una nueva lista para organizar tu página de inicio",
|
||||
"lists.create_list": "Crear lista",
|
||||
"lists.delete": "Borrar lista",
|
||||
"lists.delete": "Eliminar lista",
|
||||
"lists.done": "Hecho",
|
||||
"lists.edit": "Editar lista",
|
||||
"lists.exclusive": "Ocultar miembros en Inicio",
|
||||
@@ -928,7 +934,7 @@
|
||||
"notification_requests.title": "Notificaciones filtradas",
|
||||
"notification_requests.view": "Ver notificaciones",
|
||||
"notifications.clear": "Limpiar notificaciones",
|
||||
"notifications.clear_confirmation": "¿Seguro de querer borrar permanentemente todas tus notificaciones?",
|
||||
"notifications.clear_confirmation": "¿Estás seguro de que quieres eliminar definitivamente todas tus notificaciones?",
|
||||
"notifications.clear_title": "¿Limpiar notificaciones?",
|
||||
"notifications.column_settings.admin.report": "Nuevas denuncias:",
|
||||
"notifications.column_settings.admin.sign_up": "Registros nuevos:",
|
||||
@@ -1162,7 +1168,7 @@
|
||||
"status.context.show": "Mostrar",
|
||||
"status.continued_thread": "Hilo continuado",
|
||||
"status.copy": "Copiar enlace al estado",
|
||||
"status.delete": "Borrar",
|
||||
"status.delete": "Eliminar",
|
||||
"status.delete.success": "Publicación eliminada",
|
||||
"status.detailed_status": "Vista de conversación detallada",
|
||||
"status.direct": "Mención privada @{name}",
|
||||
@@ -1215,7 +1221,7 @@
|
||||
"status.reblogged_by": "Impulsado por {name}",
|
||||
"status.reblogs.empty": "Nadie impulsó esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
|
||||
"status.reblogs_count": "{count, plural,one {{counter} impulso} other {{counter} impulsos}}",
|
||||
"status.redraft": "Borrar y volver a borrador",
|
||||
"status.redraft": "Eliminar y volver a redactar",
|
||||
"status.remove_bookmark": "Eliminar marcador",
|
||||
"status.remove_favourite": "Eliminar de favoritos",
|
||||
"status.remove_quote": "Eliminar",
|
||||
|
||||
@@ -626,6 +626,10 @@
|
||||
"featured_carousel.header": "{count, plural,one {Publicación fijada} other {Publicaciones fijadas}}",
|
||||
"featured_carousel.slide": "Publicación {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Últimamente has publicado sobre {items}. ¿Quieres añadirlo como etiqueta destacada?",
|
||||
"featured_tags.suggestions.add": "Añadir",
|
||||
"featured_tags.suggestions.added": "Controla tus etiquetas destacadas cuando quieras en <link>Editar perfil > Etiquetas destacadas</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No, gracias",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro no se aplica al contexto en el que ha accedido a esta publlicación. Si quieres que la publicación sea filtrada también en este contexto, tendrás que editar el filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "¡El contexto no coincide!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoría de filtro ha caducado, tendrás que cambiar la fecha de caducidad para que se aplique.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Lisää enintään {count} seuraamaasi tiliä",
|
||||
"collections.accounts.empty_title": "Tämä kokoelma on tyhjä",
|
||||
"collections.collection_description": "Kuvaus",
|
||||
"collections.collection_language": "Kieli",
|
||||
"collections.collection_language_none": "Ei mikään",
|
||||
"collections.collection_name": "Nimi",
|
||||
"collections.collection_topic": "Aihe",
|
||||
"collections.confirm_account_removal": "Haluatko varmasti poistaa tämän tilin tästä kokoelmasta?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Kiinnitetty julkaisu} other {Kiinnitetyt julkaisut}}",
|
||||
"featured_carousel.slide": "Julkaisu {current, number} / {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Olet viime aikoina julkaissut aiheista {items}. Lisätäänkö nämä esiteltäviksi aihetunnisteiksi?",
|
||||
"featured_tags.suggestions.add": "Lisää",
|
||||
"featured_tags.suggestions.added": "Hallitse esiteltäviä aihetunnisteitasi milloin tahansa kohdasta <link>Muokkaa profiilia > Esiteltävät aihetunnisteet</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Ei kiitos",
|
||||
"filter_modal.added.context_mismatch_explanation": "Tämä suodatinluokka ei koske asiayhteyttä, jossa olet tarkastellut tätä julkaisua. Jos haluat julkaisun suodatettavan myös tässä asiayhteydessä, muokkaa suodatinta.",
|
||||
"filter_modal.added.context_mismatch_title": "Asiayhteys ei täsmää!",
|
||||
"filter_modal.added.expired_explanation": "Tämä suodatinluokka on vanhentunut, joten sinun on muutettava viimeistä voimassaolopäivää, jotta suodatusta käytettäisiin.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez",
|
||||
"collections.accounts.empty_title": "Cette collection est vide",
|
||||
"collections.collection_description": "Description",
|
||||
"collections.collection_language": "Langue",
|
||||
"collections.collection_language_none": "Aucune",
|
||||
"collections.collection_name": "Nom",
|
||||
"collections.collection_topic": "Sujet",
|
||||
"collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection ?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
|
||||
"featured_carousel.slide": "Message {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Récemment, vous avez posté à propos de {items}. Voulez-vous mettre ces hashtags en avant ?",
|
||||
"featured_tags.suggestions.add": "Ajouter",
|
||||
"featured_tags.suggestions.added": "Gérer vos hashtags mis en avant à tout moment depuis <link>Modifier le profil > Hashtags mis en avant</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Non merci",
|
||||
"filter_modal.added.context_mismatch_explanation": "Cette catégorie de filtre ne s'applique pas au contexte dans lequel vous avez accédé à cette publication. Si vous voulez que la publication soit filtrée dans ce contexte également, vous devrez modifier le filtre.",
|
||||
"filter_modal.added.context_mismatch_title": "Incompatibilité du contexte!",
|
||||
"filter_modal.added.expired_explanation": "Cette catégorie de filtre a expiré, vous devrez modifier la date d'expiration pour qu'elle soit appliquée.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Ajouter jusqu'à {count} comptes que vous suivez",
|
||||
"collections.accounts.empty_title": "Cette collection est vide",
|
||||
"collections.collection_description": "Description",
|
||||
"collections.collection_language": "Langue",
|
||||
"collections.collection_language_none": "Aucune",
|
||||
"collections.collection_name": "Nom",
|
||||
"collections.collection_topic": "Sujet",
|
||||
"collections.confirm_account_removal": "Voulez-vous vraiment supprimer ce compte de la collection ?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
|
||||
"featured_carousel.slide": "Message {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Récemment, vous avez posté à propos de {items}. Voulez-vous mettre ces hashtags en avant ?",
|
||||
"featured_tags.suggestions.add": "Ajouter",
|
||||
"featured_tags.suggestions.added": "Gérer vos hashtags mis en avant à tout moment depuis <link>Modifier le profil > Hashtags mis en avant</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Non merci",
|
||||
"filter_modal.added.context_mismatch_explanation": "Cette catégorie de filtre ne s'applique pas au contexte dans lequel vous avez accédé à ce message. Si vous voulez que le message soit filtré dans ce contexte également, vous devrez modifier le filtre.",
|
||||
"filter_modal.added.context_mismatch_title": "Incompatibilité du contexte !",
|
||||
"filter_modal.added.expired_explanation": "Cette catégorie de filtre a expiré, vous devrez modifier la date d'expiration pour qu'elle soit appliquée.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Cuir suas le {count} cuntas leis a leanann tú",
|
||||
"collections.accounts.empty_title": "Tá an bailiúchán seo folamh",
|
||||
"collections.collection_description": "Cur síos",
|
||||
"collections.collection_language": "Teanga",
|
||||
"collections.collection_language_none": "Dada",
|
||||
"collections.collection_name": "Ainm",
|
||||
"collections.collection_topic": "Topaic",
|
||||
"collections.confirm_account_removal": "An bhfuil tú cinnte gur mian leat an cuntas seo a bhaint den bhailiúchán seo?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Postáil phinnáilte} two {Poist Phionáilte} few {Poist Phionáilte} many {Poist Phionáilte} other {Poist Phionáilte}}",
|
||||
"featured_carousel.slide": "Post {current, number} of {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Le déanaí, tá postáil déanta agat faoi {items}. An gcuirfeá iad seo leis mar hashtags le feiceáil?",
|
||||
"featured_tags.suggestions.add": "Cuir leis",
|
||||
"featured_tags.suggestions.added": "Bainistigh do hashtags le feiceáil ag am ar bith faoi <link>Cuir Próifíl in Eagar > Hashtags le feiceáil</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Ní raibh maith agat",
|
||||
"filter_modal.added.context_mismatch_explanation": "Ní bhaineann an chatagóir scagaire seo leis an gcomhthéacs ina bhfuair tú rochtain ar an bpostáil seo. Más mian leat an postáil a scagadh sa chomhthéacs seo freisin, beidh ort an scagaire a chur in eagar.",
|
||||
"filter_modal.added.context_mismatch_title": "Neamhréir comhthéacs!",
|
||||
"filter_modal.added.expired_explanation": "Tá an chatagóir scagaire seo imithe in éag, beidh ort an dáta éaga a athrú chun é a chur i bhfeidhm.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Engade ata {count} contas que segues",
|
||||
"collections.accounts.empty_title": "A colección está baleira",
|
||||
"collections.collection_description": "Descrición",
|
||||
"collections.collection_language": "Idioma",
|
||||
"collections.collection_language_none": "Ningún",
|
||||
"collections.collection_name": "Nome",
|
||||
"collections.collection_topic": "Temática",
|
||||
"collections.confirm_account_removal": "Tes certeza de querer retirar esta conta desta colección?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Publicación fixada} other {Publicacións fixadas}}",
|
||||
"featured_carousel.slide": "Publicación {current, number} de {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Ultimamente publicaches sobre {items}. Engadimos isto como cancelos destacados?",
|
||||
"featured_tags.suggestions.add": "Engadir",
|
||||
"featured_tags.suggestions.added": "Xestiona os teus cancelos destacados cando queiras en <link>Editar perfil > Cancelos destacados</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Non, grazas",
|
||||
"filter_modal.added.context_mismatch_explanation": "Esta categoría de filtro non se aplica ao contexto no que accedeches a esta publicación. Se queres que a publicación se filtre nese contexto tamén, terás que editar o filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "Non concorda o contexto!",
|
||||
"filter_modal.added.expired_explanation": "Esta categoría de filtro caducou, terás que cambiar a data de caducidade para que se aplique.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "להוסיף עד ל־{count} חשבונות שאתם עוקבים אחריהם",
|
||||
"collections.accounts.empty_title": "האוסף הזה ריק",
|
||||
"collections.collection_description": "תיאור",
|
||||
"collections.collection_language": "שפה",
|
||||
"collections.collection_language_none": "לא מצוין",
|
||||
"collections.collection_name": "כינוי",
|
||||
"collections.collection_topic": "נושא",
|
||||
"collections.confirm_account_removal": "בוודאות להסיר חשבון זה מהאוסף?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {הודעה אחת נעוצה} two {הודעותיים נעוצות} many {הודעות נעוצות} other {הודעות נעוצות}}",
|
||||
"featured_carousel.slide": "הודעה {current, number} מתוך {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "לאחרונה פרסמת על {items}. להוסיף את הנושאים לתגיות הנבחרות?",
|
||||
"featured_tags.suggestions.add": "הוספה",
|
||||
"featured_tags.suggestions.added": "ניהול התגיות הנבחרות בכל עת תחת <link>עריכת פרופיל > תגיות נבחרות</link>.",
|
||||
"featured_tags.suggestions.dismiss": "לא תודה",
|
||||
"filter_modal.added.context_mismatch_explanation": "קטגוריית הסנן הזאת לא חלה על ההקשר שממנו הגעת אל ההודעה הזו. אם תרצה/י שההודעה תסונן גם בהקשר זה, תצטרך/י לערוך את הסנן.",
|
||||
"filter_modal.added.context_mismatch_title": "אין התאמה להקשר!",
|
||||
"filter_modal.added.expired_explanation": "פג תוקפה של קטגוריית הסינון הזו, יש צורך לשנות את תאריך התפוגה כדי שהסינון יוחל.",
|
||||
|
||||
@@ -348,6 +348,8 @@
|
||||
"collections.accounts.empty_description": "Adj hozzá legfeljebb {count} követett fiókot",
|
||||
"collections.accounts.empty_title": "Ez a gyűjtemény üres",
|
||||
"collections.collection_description": "Leírás",
|
||||
"collections.collection_language": "Nyelv",
|
||||
"collections.collection_language_none": "Egyik sem",
|
||||
"collections.collection_name": "Név",
|
||||
"collections.collection_topic": "Téma",
|
||||
"collections.confirm_account_removal": "Biztos, hogy eltávolítod ezt a fiókot ebből a gyűjteményből?",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Bættu við allt að {count} aðgöngum sem þú fylgist með",
|
||||
"collections.accounts.empty_title": "Þetta safn er tómt",
|
||||
"collections.collection_description": "Lýsing",
|
||||
"collections.collection_language": "Tungumál",
|
||||
"collections.collection_language_none": "Ekkert",
|
||||
"collections.collection_name": "Nafn",
|
||||
"collections.collection_topic": "Umfjöllunarefni",
|
||||
"collections.confirm_account_removal": "Ertu viss um að þú viljir fjarlægja þennan aðgang úr þessu safni?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Fest færsla} other {Festar færslur}}",
|
||||
"featured_carousel.slide": "Færsla {current, number} af {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Að undanförnu hefurðu skrifað um {items}. Ætti að bæta þessu við sem myllumerkjum með aukið vægi?",
|
||||
"featured_tags.suggestions.add": "Bæta við",
|
||||
"featured_tags.suggestions.added": "Sýslaðu með þau myllumerki þín sem eru með með aukið vægi með því að fara í <link>Breyta notandasniði > Myllumerki með aukið vægi</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Nei takk",
|
||||
"filter_modal.added.context_mismatch_explanation": "Þessi síuflokkur á ekki við í því samhengi sem aðgangur þinn að þessari færslu felur í sér. Ef þú vilt að færslan sé einnig síuð í þessu samhengi, þá þarftu að breyta síunni.",
|
||||
"filter_modal.added.context_mismatch_title": "Misræmi í samhengi!",
|
||||
"filter_modal.added.expired_explanation": "Þessi síuflokkur er útrunninn, þú þarft að breyta gidistímanum svo hann geti átt við.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Aggiungi fino a {count} account che segui",
|
||||
"collections.accounts.empty_title": "Questa collezione è vuota",
|
||||
"collections.collection_description": "Descrizione",
|
||||
"collections.collection_language": "Lingua",
|
||||
"collections.collection_language_none": "Nessuna",
|
||||
"collections.collection_name": "Nome",
|
||||
"collections.collection_topic": "Argomento",
|
||||
"collections.confirm_account_removal": "Si è sicuri di voler rimuovere questo account da questa collezione?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Post appuntato} other {Post appuntati}}",
|
||||
"featured_carousel.slide": "Post {current, number} di {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Ultimamente hai pubblicato contenuti relativi a {items}. Vuoi aggiungerli come hashtag in evidenza?",
|
||||
"featured_tags.suggestions.add": "Aggiungi",
|
||||
"featured_tags.suggestions.added": "Gestisci i tuoi hashtag in evidenza in qualsiasi momento in <link>Modifica profilo > Hashtag in evidenza</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No, grazie",
|
||||
"filter_modal.added.context_mismatch_explanation": "La categoria di questo filtro non si applica al contesto in cui hai acceduto a questo post. Se desideri che il post sia filtrato anche in questo contesto, dovrai modificare il filtro.",
|
||||
"filter_modal.added.context_mismatch_title": "Contesto non corrispondente!",
|
||||
"filter_modal.added.expired_explanation": "La categoria di questo filtro è scaduta, dovrvai modificarne la data di scadenza per applicarlo.",
|
||||
|
||||
@@ -162,7 +162,7 @@
|
||||
"account_edit.featured_hashtags.item": "hashtag",
|
||||
"account_edit.featured_hashtags.placeholder": "幫tsān別lâng認捌,kap緊緊接近使用lí收藏ê主題。",
|
||||
"account_edit.featured_hashtags.title": "特色ê hashtag",
|
||||
"account_edit.field_delete_modal.confirm": "Lí敢確定behthâi掉tsit ê自訂ê框á?Tsit ê動作bē當改倒轉。",
|
||||
"account_edit.field_delete_modal.confirm": "Lí敢確定beh thâi掉tsit ê自訂ê框á?Tsit ê動作bē當改倒轉。",
|
||||
"account_edit.field_delete_modal.delete_button": "Thâi掉",
|
||||
"account_edit.field_delete_modal.title": "敢beh thâi掉自訂ê框á?",
|
||||
"account_edit.field_edit_modal.add_title": "加自訂ê框á",
|
||||
@@ -183,6 +183,15 @@
|
||||
"account_edit.field_reorder_modal.drag_start": "有揀框á「{item}」。",
|
||||
"account_edit.field_reorder_modal.handle_label": "Giú框á「{item}」",
|
||||
"account_edit.field_reorder_modal.title": "重整理框á",
|
||||
"account_edit.image_alt_modal.add_title": "加添說明文字",
|
||||
"account_edit.image_alt_modal.details_content": "著愛:<ul> <li>照圖描述家kī</li> <li>用第三人稱(比如用「阿明」,毋是「我」)</li> <li>束結,幾个詞就夠ah</li> </ul> 毋通:<ul> <li>用「……ê相片」結束,這對讀螢幕程式是加講ê</li> </ul> 例:<ul> <li>「阿明穿青ê siá-tsuh,koh掛眼鏡」</li> </ul>",
|
||||
"account_edit.image_alt_modal.details_title": "撇步:個人資料ê相片 ê 說明文字",
|
||||
"account_edit.image_alt_modal.edit_title": "編說明文字",
|
||||
"account_edit.image_alt_modal.text_hint": "ALT說明文字通幫tsān讀螢幕程式ê用者知影lí ê內容。",
|
||||
"account_edit.image_alt_modal.text_label": "替代文字",
|
||||
"account_edit.image_delete_modal.confirm": "Lí敢確定beh thâi掉tsit幅圖?Tsit ê動作bē當改倒轉。",
|
||||
"account_edit.image_delete_modal.delete_button": "Thâi掉",
|
||||
"account_edit.image_delete_modal.title": "Kám beh thâi掉影像?",
|
||||
"account_edit.image_edit.add_button": "加圖片",
|
||||
"account_edit.image_edit.alt_add_button": "加添說明文字",
|
||||
"account_edit.image_edit.alt_edit_button": "編說明文字",
|
||||
@@ -202,6 +211,16 @@
|
||||
"account_edit.profile_tab.subtitle": "自訂lí ê個人資料ê分頁kap顯示ê內容。",
|
||||
"account_edit.profile_tab.title": "個人資料分頁設定",
|
||||
"account_edit.save": "儲存",
|
||||
"account_edit.upload_modal.back": "轉去",
|
||||
"account_edit.upload_modal.done": "做好ah",
|
||||
"account_edit.upload_modal.next": "後一步",
|
||||
"account_edit.upload_modal.step_crop.zoom": "伸kiu",
|
||||
"account_edit.upload_modal.step_upload.button": "瀏覽檔案",
|
||||
"account_edit.upload_modal.step_upload.dragging": "Giú kàu tsia傳上去",
|
||||
"account_edit.upload_modal.step_upload.header": "揀圖片",
|
||||
"account_edit.upload_modal.step_upload.hint": "WEBP、PNG、GIF á是 JPG 格式,上大 {limit}MB。{br}圖會伸kiu kàu {width}x{height} px。",
|
||||
"account_edit.upload_modal.title_add": "加個人資料ê相",
|
||||
"account_edit.upload_modal.title_replace": "替換個人資料ê相",
|
||||
"account_edit.verified_modal.details": "用驗證連kàu個人網站ê連結來加添lí ê Mastodon個人檔案ê通信ê程度。下kha是運作ê方法:",
|
||||
"account_edit.verified_modal.invisible_link.details": "加連結kàu lí ê網頁頭(header)。上重要ê部份是 rel=\"me\",伊防止通過用者生成ê網站內容來做假包。Lí甚至佇網頁ê header毋免用 {tag},反轉用link標簽,但是HTML定著佇無執行JavaScript ê時陣,就ē當接近使用。",
|
||||
"account_edit.verified_modal.invisible_link.summary": "Án-tsuánn khàm掉tsit ê連結?",
|
||||
@@ -330,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "加lí跟tuè ê口座,上tsē {count} ê",
|
||||
"collections.accounts.empty_title": "收藏內底無半項",
|
||||
"collections.collection_description": "說明",
|
||||
"collections.collection_language": "語言",
|
||||
"collections.collection_language_none": "無",
|
||||
"collections.collection_name": "名",
|
||||
"collections.collection_topic": "主題",
|
||||
"collections.confirm_account_removal": "Lí確定beh對收藏suá掉tsit ê口座?",
|
||||
@@ -488,7 +509,7 @@
|
||||
"confirmations.quiet_post_quote_info.message": "Nā是引用無tī公共時間線內底êPO文,lí êPO文bē當tī趨勢ê時間線顯示。",
|
||||
"confirmations.quiet_post_quote_info.title": "引用無tī公開ê時間線內底顯示ê PO文",
|
||||
"confirmations.redraft.confirm": "Thâi掉了後重寫",
|
||||
"confirmations.redraft.message": "Lí kám確定behthâi掉tsit篇PO文了後koh重寫?收藏kap轉PO ē無去,而且原底ê PO文ê回應ē變孤立。",
|
||||
"confirmations.redraft.message": "Lí kám確定beh thâi掉tsit篇PO文了後koh重寫?收藏kap轉PO ē無去,而且原底ê PO文ê回應ē變孤立。",
|
||||
"confirmations.redraft.title": "Kám beh thâi掉koh重寫PO文?",
|
||||
"confirmations.remove_from_followers.confirm": "Suá掉跟tuè lí ê",
|
||||
"confirmations.remove_from_followers.message": "{name} ē停止跟tuè lí。Lí kám確定beh繼續?",
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
"account_edit.custom_fields.name": "veld",
|
||||
"account_edit.custom_fields.placeholder": "Voeg je voornaamwoorden, externe links of iets anders toe dat je wilt delen.",
|
||||
"account_edit.custom_fields.reorder_button": "Velden opnieuw ordenen",
|
||||
"account_edit.custom_fields.tip_content": "Je kunt gemakkelijk geloofwaardigheid toevoegen aan je Mastodon account door links te controleren naar websites die je bezit.",
|
||||
"account_edit.custom_fields.tip_content": "Je kunt gemakkelijk je Mastodon-account geloofwaardig maken door links naar websites die van jou zijn te laten verifiëren.",
|
||||
"account_edit.custom_fields.tip_title": "Tip: Geverifieerde links toevoegen",
|
||||
"account_edit.custom_fields.title": "Extra velden",
|
||||
"account_edit.custom_fields.verified_hint": "Hoe voeg ik een geverifieerde link toe?",
|
||||
@@ -179,15 +179,15 @@
|
||||
"account_edit.field_reorder_modal.drag_end": "Veld \"{item}\" werd geschrapt.",
|
||||
"account_edit.field_reorder_modal.drag_instructions": "Druk op spatie of enter om de aangepaste velden te herschikken. Gebruik de pijltjestoetsen om het veld omhoog of omlaag te verplaatsen. Druk opnieuw op spatie of enter om het veld op diens nieuwe positie te laten vallen, of druk op escape om te annuleren.",
|
||||
"account_edit.field_reorder_modal.drag_move": "Veld \"{item}\" is verplaatst.",
|
||||
"account_edit.field_reorder_modal.drag_over": "Veld \"{item}\" is over \"{over} \" verplaatst.",
|
||||
"account_edit.field_reorder_modal.drag_over": "Veld \"{item}\" is over \"{over}\" geplaatst.",
|
||||
"account_edit.field_reorder_modal.drag_start": "Opgepakt veld \"{item}\".",
|
||||
"account_edit.field_reorder_modal.handle_label": "Veld \"{item}\" slepen",
|
||||
"account_edit.field_reorder_modal.title": "Velden herschikken",
|
||||
"account_edit.image_alt_modal.add_title": "Alt-tekst toevoegen",
|
||||
"account_edit.image_alt_modal.details_content": "DOEN: <ul> <li>Beschrijf jezelf zoals afgebeeld</li> <li>Gebruik taal van een derde persoon (bijv. “Alex” in plaats van “ik”)</li> <li>Wees beknopt - een paar woorden is vaak genoeg</li> </ul> NIET DOEN: <ul> <li>Begin met “Foto van” - dit is overbodig voor schermlezers</li> </ul> VOORBEELD: <ul> <li>“Alex draagt een groen shirt en een bril”</li> </ul>",
|
||||
"account_edit.image_alt_modal.details_content": "DOEN: <ul> <li>Beschrijf jezelf zoals afgebeeld</li> <li>Schrijf in de derde persoon (bijv. “Alex” in plaats van “ik”)</li> <li>Wees beknopt - een paar woorden is vaak genoeg</li> </ul> NIET DOEN: <ul> <li>Begin met “Foto van” - dit is overbodig voor schermlezers</li> </ul> VOORBEELD: <ul> <li>“Alex draagt een groen shirt en een bril”</li> </ul>",
|
||||
"account_edit.image_alt_modal.details_title": "Tips: Alt-tekst voor profielfoto's",
|
||||
"account_edit.image_alt_modal.edit_title": "Alt-tekst bewerken",
|
||||
"account_edit.image_alt_modal.text_hint": "Alt-text helpt gebruikers van schermlezer jouw inhoud te begrijpen.",
|
||||
"account_edit.image_alt_modal.text_hint": "Alt-text helpt gebruikers van schermlezers jouw inhoud te begrijpen.",
|
||||
"account_edit.image_alt_modal.text_label": "Alt-tekst",
|
||||
"account_edit.image_delete_modal.confirm": "Weet je zeker dat je deze afbeelding wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"account_edit.image_delete_modal.delete_button": "Verwijderen",
|
||||
@@ -221,10 +221,10 @@
|
||||
"account_edit.upload_modal.step_upload.hint": "WEBP-, PNG-, GIF- of JPG-formaat, tot max. {limit}MB.{br}Afbeelding wordt geschaald naar {width}x{height}px.",
|
||||
"account_edit.upload_modal.title_add": "Profielfoto toevoegen",
|
||||
"account_edit.upload_modal.title_replace": "Profielfoto vervangen",
|
||||
"account_edit.verified_modal.details": "Voeg geloofwaardigheid toe aan je Mastodonprofiel door links naar persoonlijke websites te verifiëren. Zo werkt het:",
|
||||
"account_edit.verified_modal.invisible_link.details": "Voeg de link toe aan uw header. Het belangrijke onderdeel is rel=\"me\" om te voorkomen dat websites impersoneren met door de gebruiker gegenereerde inhoud. Je kunt zelfs een linktag gebruiken in de kop van de pagina in plaats van {tag}, maar de HTML moet toegankelijk zijn zonder JavaScript uit te voeren.",
|
||||
"account_edit.verified_modal.details": "Maak je Mastodonprofiel geloofwaardig door links naar persoonlijke websites te verifiëren. Zo werkt het:",
|
||||
"account_edit.verified_modal.invisible_link.details": "Voeg de link aan de HTML van je website toe. Het belangrijkste onderdeel is rel=\"me\", waarmee wordt voorkomen dat websites met user-generated content geïmpersoneerd kunnen worden. Je kunt zelfs een <link>-tag gebruiken binnen de <head>-tag van je website in plaats van {tag}, maar de HTML moet zonder JavaScript toegankelijk zijn.",
|
||||
"account_edit.verified_modal.invisible_link.summary": "Hoe maak ik de link onzichtbaar?",
|
||||
"account_edit.verified_modal.step1.header": "Kopieer de onderstaande HTML-code en plak deze in de koptekst van je website",
|
||||
"account_edit.verified_modal.step1.header": "Kopieer de onderstaande HTML-code en plak deze binnen de <head>-tag van je website",
|
||||
"account_edit.verified_modal.step2.details": "Als je je website al als een aangepast veld hebt toegevoegd, moet je deze verwijderen en opnieuw toevoegen om de verificatie te activeren.",
|
||||
"account_edit.verified_modal.step2.header": "Voeg je website toe als een aangepast veld",
|
||||
"account_edit.verified_modal.title": "Hoe voeg je een geverifieerde link toe",
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Tot {count} accounts die je volgt toevoegen",
|
||||
"collections.accounts.empty_title": "Deze verzameling is leeg",
|
||||
"collections.collection_description": "Omschrijving",
|
||||
"collections.collection_language": "Taal",
|
||||
"collections.collection_language_none": "Geen",
|
||||
"collections.collection_name": "Naam",
|
||||
"collections.collection_topic": "Onderwerp",
|
||||
"collections.confirm_account_removal": "Weet je zeker dat je dit account uit deze verzameling wilt verwijderen?",
|
||||
|
||||
@@ -344,6 +344,8 @@
|
||||
"collections.account_count": "{count, plural, one {# llogari} other {# llogari}}",
|
||||
"collections.accounts.empty_title": "Ky koleksion është i zbrazët",
|
||||
"collections.collection_description": "Përshkrim",
|
||||
"collections.collection_language": "Gjuhë",
|
||||
"collections.collection_language_none": "Asnjë",
|
||||
"collections.collection_name": "Emër",
|
||||
"collections.collection_topic": "Temë",
|
||||
"collections.confirm_account_removal": "Jeni i sigurt se doni të hiqet kjo llogari nga ky koleksion?",
|
||||
|
||||
@@ -277,6 +277,8 @@
|
||||
"closed_registrations_modal.preamble": "Mastodon är decentraliserat så oavsett var du skapar ditt konto kommer du att kunna följa och interagera med någon på denna server. Du kan också köra din egen server!",
|
||||
"closed_registrations_modal.title": "Registrera sig på Mastodon",
|
||||
"collections.accounts.empty_description": "Lägg till upp till {count} konton som du följer",
|
||||
"collections.collection_language": "Språk",
|
||||
"collections.collection_language_none": "Inga",
|
||||
"collections.create_a_collection_hint": "Skapa en samling för att rekommendera eller dela dina favoritkonton med andra.",
|
||||
"collections.create_collection": "Skapa samling",
|
||||
"collections.delete_collection": "Radera samling",
|
||||
@@ -501,6 +503,10 @@
|
||||
"featured_carousel.current": "<sr>Inlägg</sr> {current, number} / {max, number}",
|
||||
"featured_carousel.header": "{count, plural,one {Fäst inlägg} other {Fästa inlägg}}",
|
||||
"featured_carousel.slide": "Inlägg {current, number} av {max, number}",
|
||||
"featured_tags.suggestions": "På senare tid har du skrivit om {items}. Lägg till dessa som utvalda hashtaggar?",
|
||||
"featured_tags.suggestions.add": "Lägg till",
|
||||
"featured_tags.suggestions.added": "Hantera dina utvalda hashtaggar när som helst under <link>Redigera profil > Utvalda hashtaggar</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Nej tack",
|
||||
"filter_modal.added.context_mismatch_explanation": "Denna filterkategori gäller inte för det sammanhang där du har tillgång till det här inlägget. Om du vill att inlägget ska filtreras även i detta sammanhang måste du redigera filtret.",
|
||||
"filter_modal.added.context_mismatch_title": "Misspassning av sammanhang!",
|
||||
"filter_modal.added.expired_explanation": "Denna filterkategori har utgått, du måste ändra utgångsdatum för att den ska kunna tillämpas.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Takip ettiğiniz hesapların sayısını {count} kadar artırın",
|
||||
"collections.accounts.empty_title": "Bu koleksiyon boş",
|
||||
"collections.collection_description": "Açıklama",
|
||||
"collections.collection_language": "Dil",
|
||||
"collections.collection_language_none": "Hiçbiri",
|
||||
"collections.collection_name": "Ad",
|
||||
"collections.collection_topic": "Konu",
|
||||
"collections.confirm_account_removal": "Bu hesabı bu koleksiyondan çıkarmak istediğinizden emin misiniz?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {{counter} Sabitlenmiş Gönderi} other {{counter} Sabitlenmiş Gönderi}}",
|
||||
"featured_carousel.slide": "Gönderi {current, number} / {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Son zamanlarda {items} hakkında gönderileriniz var. Bunlar öne çıkan etiketler olarak eklensin mi?",
|
||||
"featured_tags.suggestions.add": "Ekle",
|
||||
"featured_tags.suggestions.added": "Öne çıkan etiketlerinizi istediğiniz zaman <link>Profil Düzenle > Öne çıkan etiketler</link> bölümünden yönetebilirsiniz.",
|
||||
"featured_tags.suggestions.dismiss": "Hayır teşekkürler",
|
||||
"filter_modal.added.context_mismatch_explanation": "Bu süzgeç kategorisi, bu gönderide eriştiğin bağlama uymuyor. Eğer gönderinin bu bağlamda da filtrelenmesini istiyorsanız, süzgeci düzenlemeniz gerekiyor.",
|
||||
"filter_modal.added.context_mismatch_title": "Bağlam uyumsuzluğu!",
|
||||
"filter_modal.added.expired_explanation": "Bu süzgeç kategorisinin süresi dolmuş, süzgeci uygulamak için bitiş tarihini değiştirmeniz gerekiyor.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "Thêm tối đa {count} tài khoản mà bạn theo dõi",
|
||||
"collections.accounts.empty_title": "Gói khởi đầu này trống",
|
||||
"collections.collection_description": "Mô tả",
|
||||
"collections.collection_language": "Ngôn ngữ",
|
||||
"collections.collection_language_none": "Không",
|
||||
"collections.collection_name": "Tên",
|
||||
"collections.collection_topic": "Chủ đề",
|
||||
"collections.confirm_account_removal": "Bạn có chắc muốn gỡ tài khoản này khỏi gói khởi đầu?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, other {Tút đã ghim}}",
|
||||
"featured_carousel.slide": "Tút {current, number} trong {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Gần đây bạn đã đăng về {items}. Thêm cái này vào hashtag thường dùng?",
|
||||
"featured_tags.suggestions.add": "Thêm",
|
||||
"featured_tags.suggestions.added": "Quản lý hashtag bạn thường dùng vào bất cứ lúc nào tại <link>Sửa hồ sơ > Hashtag thường dùng</link>.",
|
||||
"featured_tags.suggestions.dismiss": "Không, cảm ơn",
|
||||
"filter_modal.added.context_mismatch_explanation": "Danh mục bộ lọc này không áp dụng cho ngữ cảnh mà bạn đã truy cập tút này. Nếu bạn muốn tút cũng được lọc trong ngữ cảnh này, bạn sẽ phải chỉnh sửa bộ lọc.",
|
||||
"filter_modal.added.context_mismatch_title": "Bối cảnh không phù hợp!",
|
||||
"filter_modal.added.expired_explanation": "Danh mục bộ lọc này đã hết hạn, bạn sẽ cần thay đổi ngày hết hạn để áp dụng.",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "添加你关注的账号,最多 {count} 个",
|
||||
"collections.accounts.empty_title": "收藏列表为空",
|
||||
"collections.collection_description": "说明",
|
||||
"collections.collection_language": "语言",
|
||||
"collections.collection_language_none": "无",
|
||||
"collections.collection_name": "名称",
|
||||
"collections.collection_topic": "话题",
|
||||
"collections.confirm_account_removal": "你确定要将从收藏列表中移除此账号吗?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, other {# 条置顶嘟文}}",
|
||||
"featured_carousel.slide": "第 {current, number} 条嘟文,共 {max, number} 条",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "你最近发布了有关{items}的内容。要将这些添加为精选的话题标签吗?",
|
||||
"featured_tags.suggestions.add": "添加",
|
||||
"featured_tags.suggestions.added": "你可以在<link>修改个人资料 > 精选的话题标签</link>下随时管理你精选的话题标签。",
|
||||
"featured_tags.suggestions.dismiss": "不了,谢谢",
|
||||
"filter_modal.added.context_mismatch_explanation": "这条过滤规则不适用于你当前访问此嘟文的场景。要在此场景下过滤嘟文,你必须编辑此过滤规则。",
|
||||
"filter_modal.added.context_mismatch_title": "场景不匹配!",
|
||||
"filter_modal.added.expired_explanation": "此过滤规则类别已过期,你需要修改到期日期才能应用。",
|
||||
|
||||
@@ -349,6 +349,8 @@
|
||||
"collections.accounts.empty_description": "加入最多 {count} 個您跟隨之帳號",
|
||||
"collections.accounts.empty_title": "此收藏名單是空的",
|
||||
"collections.collection_description": "說明",
|
||||
"collections.collection_language": "語言",
|
||||
"collections.collection_language_none": "無",
|
||||
"collections.collection_name": "名稱",
|
||||
"collections.collection_topic": "主題",
|
||||
"collections.confirm_account_removal": "您是否確定要自此收藏名單中移除此帳號?",
|
||||
@@ -626,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, other {# 則釘選嘟文}}",
|
||||
"featured_carousel.slide": "{max, number} 則嘟文中之第 {current, number} 則",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "最近您曾發有關 {items} 之嘟文。是否新增其為推薦主題標籤?",
|
||||
"featured_tags.suggestions.add": "新增",
|
||||
"featured_tags.suggestions.added": "於 <link>編輯個人檔案 > 推薦主題標籤</link> 隨時管理您的推薦主題標籤。",
|
||||
"featured_tags.suggestions.dismiss": "不需要,謝謝",
|
||||
"filter_modal.added.context_mismatch_explanation": "此過濾器類別不是用您所存取嘟文的情境。若您想要此嘟文被於此情境被過濾,您必須編輯過濾器。",
|
||||
"filter_modal.added.context_mismatch_title": "不符合情境!",
|
||||
"filter_modal.added.expired_explanation": "此過濾器類別已失效,您需要更新過期日期以套用。",
|
||||
|
||||
@@ -104,24 +104,24 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||
builder.addCase(addFeaturedTags.pending, (state) => {
|
||||
state.isPending = true;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.rejected, (state) => {
|
||||
builder.addCase(addFeaturedTags.rejected, (state) => {
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
||||
builder.addCase(addFeaturedTags.fulfilled, (state, action) => {
|
||||
if (!state.profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.profile.featuredTags = [
|
||||
...state.profile.featuredTags,
|
||||
transformTag(action.payload),
|
||||
...action.payload.map(transformTag),
|
||||
].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
if (state.tagSuggestions) {
|
||||
state.tagSuggestions = state.tagSuggestions.filter(
|
||||
(tag) => tag.name !== action.meta.arg.name,
|
||||
(tag) => !action.meta.arg.names.includes(tag.name),
|
||||
);
|
||||
}
|
||||
state.isPending = false;
|
||||
@@ -350,20 +350,11 @@ export const fetchSuggestedTags = createDataLoadingThunk(
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const addFeaturedTag = createDataLoadingThunk(
|
||||
export const addFeaturedTags = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/addFeaturedTag`,
|
||||
({ name }: { name: string }) => apiPostFeaturedTag(name),
|
||||
{
|
||||
condition(arg, { getState }) {
|
||||
const state = getState();
|
||||
return (
|
||||
!!state.profileEdit.profile &&
|
||||
!state.profileEdit.profile.featuredTags.some(
|
||||
(tag) => tag.name === arg.name,
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
({ names }: { names: string[] }) =>
|
||||
Promise.all(names.map((n) => apiPostFeaturedTag(n))),
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const deleteFeaturedTag = createDataLoadingThunk(
|
||||
|
||||
@@ -51,13 +51,17 @@ export const mockHandlers = {
|
||||
'/packs-dev/emoji/:locale.json',
|
||||
async ({ params }) => {
|
||||
const locale = toSupportedLocale(params.locale);
|
||||
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
|
||||
const emojiModules = import.meta.glob<CompactEmoji[]>(
|
||||
'../../../../../node_modules/emojibase-data/**/compact.json',
|
||||
{ import: 'default' },
|
||||
);
|
||||
const path = emojiModules[key];
|
||||
if (!path) {
|
||||
throw new Error(`Unsupported locale: ${locale}`);
|
||||
}
|
||||
action('fetching emoji data')(locale);
|
||||
const { default: data } = (await import(
|
||||
/* @vite-ignore */
|
||||
`emojibase-data/${locale}/compact.json`
|
||||
)) as {
|
||||
default: CompactEmoji[];
|
||||
};
|
||||
const data = await path();
|
||||
|
||||
return HttpResponse.json([data]);
|
||||
},
|
||||
|
||||
@@ -42,6 +42,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
def process_status
|
||||
@tags = []
|
||||
@mentions = []
|
||||
@tagged_objects = []
|
||||
@unresolved_mentions = []
|
||||
@silenced_account_ids = []
|
||||
@params = {}
|
||||
@@ -56,6 +57,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
ApplicationRecord.transaction do
|
||||
@status = Status.create!(@params.merge(quote: @quote))
|
||||
attach_tags(@status)
|
||||
attach_tagged_objects(@status)
|
||||
attach_mentions(@status)
|
||||
attach_counts(@status)
|
||||
end
|
||||
@@ -181,6 +183,13 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
end
|
||||
|
||||
def attach_tagged_objects(status)
|
||||
@tagged_objects.each do |tagged_object|
|
||||
tagged_object.status = status
|
||||
tagged_object.save
|
||||
end
|
||||
end
|
||||
|
||||
def attach_mentions(status)
|
||||
@mentions.each do |mention|
|
||||
mention.status = status
|
||||
@@ -210,6 +219,8 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
process_mention tag
|
||||
elsif equals_or_includes?(tag['type'], 'Emoji')
|
||||
process_emoji tag
|
||||
elsif equals_or_includes?(tag['type'], 'FeaturedCollection')
|
||||
process_tagged_collection tag
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -266,6 +277,15 @@ class ActivityPub::Activity::Create < ActivityPub::Activity
|
||||
end
|
||||
end
|
||||
|
||||
def process_tagged_collection(tag)
|
||||
return if tag['id'].blank?
|
||||
|
||||
# TODO: We probably want to resolve unknown objects and push them to an `@unresolved_tagged_objects` on failure
|
||||
collection = ActivityPub::TagManager.instance.uri_to_resource(tag['id'], Collection)
|
||||
|
||||
@tagged_objects << TaggedObject.new(uri: ActivityPub::TagManager.instance.uri_for(collection), object: collection, ap_type: 'FeaturedCollection') if collection.present?
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
return [] if @object['attachment'].nil?
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
update_account
|
||||
elsif supported_object_type? || converted_object_type?
|
||||
update_status
|
||||
elsif equals_or_includes_any?(@object['type'], ['FeaturedCollection']) && Mastodon::Feature.collections_federation_enabled?
|
||||
update_collection
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,6 +43,12 @@ class ActivityPub::Activity::Update < ActivityPub::Activity
|
||||
ActivityPub::ProcessStatusUpdateService.new.call(@status, @json, @object, request_id: @options[:request_id])
|
||||
end
|
||||
|
||||
def update_collection
|
||||
return reject_payload! if non_matching_uri_hosts?(@account.uri, object_uri)
|
||||
|
||||
ActivityPub::ProcessFeaturedCollectionService.new.call(@account, @object)
|
||||
end
|
||||
|
||||
def object_too_old?
|
||||
@object['published'].present? && @object['published'].to_datetime < OBJECT_AGE_THRESHOLD.ago
|
||||
rescue Date::Error
|
||||
|
||||
@@ -30,7 +30,7 @@ class ActivityPub::Parser::MediaAttachmentParser
|
||||
|
||||
def description
|
||||
str = @json['summary'].presence || @json['name'].presence
|
||||
str = str.strip[0...MediaAttachment::MAX_DESCRIPTION_LENGTH] if str.present?
|
||||
str = str.strip[0...MediaAttachment::MAX_DESCRIPTION_HARD_LENGTH_LIMIT] if str.present?
|
||||
str
|
||||
end
|
||||
|
||||
|
||||
@@ -262,6 +262,14 @@ class ActivityPub::TagManager
|
||||
uri_to_resource(uri, Account)
|
||||
end
|
||||
|
||||
def uri_to_local_collection(uri)
|
||||
path_params = Rails.application.routes.recognize_path(uri)
|
||||
return unless path_params[:controller] == 'collections'
|
||||
|
||||
# TODO: check account, but this requires handling potentially two different schemes
|
||||
Collection.find_by(id: path_params[:id])
|
||||
end
|
||||
|
||||
def uri_to_local_conversation(uri)
|
||||
path_params = Rails.application.routes.recognize_path(uri)
|
||||
return unless path_params[:controller] == 'activitypub/contexts'
|
||||
@@ -279,6 +287,8 @@ class ActivityPub::TagManager
|
||||
uris_to_local_accounts([uri]).first
|
||||
when 'Conversation'
|
||||
uri_to_local_conversation(uri)
|
||||
when 'Collection'
|
||||
uri_to_local_collection(uri)
|
||||
else
|
||||
StatusFinder.new(uri).status
|
||||
end
|
||||
|
||||
@@ -25,7 +25,7 @@ module Account::Avatar
|
||||
validates_attachment_size :avatar, less_than: AVATAR_LIMIT
|
||||
remotable_attachment :avatar, AVATAR_LIMIT, suppress_errors: false
|
||||
|
||||
validates :avatar_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }
|
||||
validates :avatar_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }, if: -> { local? && will_save_change_to_avatar_description? }
|
||||
end
|
||||
|
||||
def avatar_original_url
|
||||
|
||||
@@ -26,7 +26,7 @@ module Account::Header
|
||||
validates_attachment_size :header, less_than: HEADER_LIMIT
|
||||
remotable_attachment :header, HEADER_LIMIT, suppress_errors: false
|
||||
|
||||
validates :header_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }
|
||||
validates :header_description, length: { maximum: MediaAttachment::MAX_DESCRIPTION_LENGTH }, if: -> { local? && will_save_change_to_header_description? }
|
||||
end
|
||||
|
||||
def header_original_url
|
||||
|
||||
@@ -231,7 +231,7 @@ module Account::Interactions
|
||||
|
||||
Rails.cache.fetch("followers_hash:#{id}:#{url_prefix}/") do
|
||||
digest = "\x00" * 32
|
||||
followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(url_prefix)}/%", false, true)).or(followers.where(uri: url_prefix)).pluck_each(:uri) do |uri|
|
||||
followers.matches_uri_prefix(url_prefix).pluck_each(:uri) do |uri|
|
||||
Xorcist.xor!(digest, Digest::SHA256.digest(uri))
|
||||
end
|
||||
digest.unpack1('H*')
|
||||
|
||||
@@ -39,6 +39,7 @@ class MediaAttachment < ApplicationRecord
|
||||
enum :processing, { queued: 0, in_progress: 1, complete: 2, failed: 3 }, prefix: true
|
||||
|
||||
MAX_DESCRIPTION_LENGTH = 1_500
|
||||
MAX_DESCRIPTION_HARD_LENGTH_LIMIT = 10_000
|
||||
|
||||
IMAGE_LIMIT = (ENV['MAX_IMAGE_SIZE'] || 16.megabytes).to_i
|
||||
VIDEO_LIMIT = (ENV['MAX_VIDEO_SIZE'] || 99.megabytes).to_i
|
||||
|
||||
@@ -47,6 +47,20 @@ class Status < ApplicationRecord
|
||||
include Status::Visibility
|
||||
include Status::InteractionPolicyConcern
|
||||
|
||||
CACHEABLE_ASSOCIATIONS = [
|
||||
:application,
|
||||
:conversation,
|
||||
:media_attachments,
|
||||
:preloadable_poll,
|
||||
:status_stat,
|
||||
:tags,
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: :account,
|
||||
tagged_objects: :object,
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
quote: { status: { account: [:account_stat, user: :role] } },
|
||||
].freeze
|
||||
|
||||
MEDIA_ATTACHMENTS_LIMIT = 4
|
||||
|
||||
rate_limit by: :account, family: :statuses
|
||||
@@ -82,6 +96,7 @@ class Status < ApplicationRecord
|
||||
has_many :mentions, dependent: :destroy, inverse_of: :status
|
||||
has_many :mentioned_accounts, through: :mentions, source: :account, class_name: 'Account'
|
||||
has_many :media_attachments, dependent: :nullify
|
||||
has_many :tagged_objects, dependent: :destroy
|
||||
has_many :quotes, foreign_key: 'quoted_status_id', inverse_of: :quoted_status, dependent: :nullify
|
||||
|
||||
# The `dependent` option is enabled by the initial `mentions` association declaration
|
||||
@@ -169,29 +184,11 @@ class Status < ApplicationRecord
|
||||
# the `dependent: destroy` callbacks remove relevant records
|
||||
before_destroy :unlink_from_conversations!, prepend: true
|
||||
|
||||
cache_associated :application,
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:tags,
|
||||
:preloadable_poll,
|
||||
quote: { status: { account: [:account_stat, user: :role] } },
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: :account,
|
||||
reblog: [
|
||||
:application,
|
||||
:media_attachments,
|
||||
:conversation,
|
||||
:status_stat,
|
||||
:tags,
|
||||
:preloadable_poll,
|
||||
quote: { status: { account: [:account_stat, user: :role] } },
|
||||
preview_cards_status: { preview_card: { author_account: [:account_stat, user: :role] } },
|
||||
account: [:account_stat, user: :role],
|
||||
active_mentions: :account,
|
||||
],
|
||||
thread: :account
|
||||
cache_associated(
|
||||
*CACHEABLE_ASSOCIATIONS,
|
||||
reblog: [*CACHEABLE_ASSOCIATIONS],
|
||||
thread: :account
|
||||
)
|
||||
|
||||
delegate :domain, :indexable?, to: :account, prefix: true
|
||||
|
||||
|
||||
19
app/models/tagged_object.rb
Normal file
19
app/models/tagged_object.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: tagged_objects
|
||||
#
|
||||
# id :bigint(8) not null, primary key
|
||||
# ap_type :string not null
|
||||
# object_type :string
|
||||
# uri :string
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# object_id :bigint(8)
|
||||
# status_id :bigint(8) not null
|
||||
#
|
||||
class TaggedObject < ApplicationRecord
|
||||
belongs_to :status, inverse_of: :tagged_objects
|
||||
belongs_to :object, polymorphic: true, optional: true
|
||||
end
|
||||
@@ -8,7 +8,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
|
||||
context_extensions :manually_approves_followers, :featured, :also_known_as,
|
||||
:moved_to, :property_value, :discoverable, :suspended,
|
||||
:memorial, :indexable, :attribution_domains
|
||||
:memorial, :indexable, :attribution_domains, :profile_settings
|
||||
|
||||
context_extensions :interaction_policies if Mastodon::Feature.collections_enabled?
|
||||
|
||||
@@ -16,7 +16,10 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
|
||||
:inbox, :outbox, :featured, :featured_tags,
|
||||
:preferred_username, :name, :summary,
|
||||
:url, :manually_approves_followers,
|
||||
:discoverable, :indexable, :published, :memorial
|
||||
:discoverable, :indexable, :published, :memorial,
|
||||
:show_featured, :show_media
|
||||
|
||||
attribute :show_media_replies, key: :show_replies_in_media
|
||||
|
||||
attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? }
|
||||
|
||||
@@ -151,7 +151,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
|
||||
def virtual_tags
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis
|
||||
object.active_mentions.to_a.sort_by(&:id) + object.tags + object.emojis + object.tagged_objects.map(&:object)
|
||||
end
|
||||
|
||||
def atom_uri
|
||||
@@ -361,8 +361,9 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer
|
||||
end
|
||||
class CustomEmojiSerializer < ActivityPub::EmojiSerializer; end
|
||||
|
||||
class CollectionSerializer < ActivityPub::FeaturedCollectionSerializer; end
|
||||
|
||||
class OptionSerializer < ActivityPub::Serializer
|
||||
class RepliesSerializer < ActivityPub::Serializer
|
||||
|
||||
@@ -5,7 +5,7 @@ class REST::CollectionSerializer < ActiveModel::Serializer
|
||||
:local, :sensitive, :discoverable, :item_count,
|
||||
:created_at, :updated_at
|
||||
|
||||
belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer
|
||||
belongs_to :tag, serializer: REST::ShallowTagSerializer
|
||||
|
||||
has_many :items, serializer: REST::CollectionItemSerializer
|
||||
|
||||
|
||||
11
app/serializers/rest/shallow_tag_serializer.rb
Normal file
11
app/serializers/rest/shallow_tag_serializer.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class REST::ShallowTagSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :name, :url
|
||||
|
||||
def url
|
||||
tag_url(object)
|
||||
end
|
||||
end
|
||||
@@ -30,6 +30,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
has_many :ordered_mentions, key: :mentions
|
||||
has_many :tags
|
||||
has_many :emojis, serializer: REST::CustomEmojiSerializer
|
||||
has_many :tagged_collections, serializer: REST::CollectionSerializer
|
||||
|
||||
# Due to a ActiveModel::Serializer quirk, if you change any of the following, have a look at
|
||||
# updating `app/serializers/rest/shallow_status_serializer.rb` as well
|
||||
@@ -170,6 +171,10 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
object.active_mentions.to_a.sort_by(&:id)
|
||||
end
|
||||
|
||||
def tagged_collections
|
||||
object.tagged_objects.filter_map { |tagged_object| tagged_object.object if tagged_object.ap_type == 'FeaturedCollection' }
|
||||
end
|
||||
|
||||
def quote_approval
|
||||
{
|
||||
automatic: object.proper.quote_policy_as_keys(:automatic),
|
||||
@@ -212,13 +217,5 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
||||
end
|
||||
end
|
||||
|
||||
class TagSerializer < ActiveModel::Serializer
|
||||
include RoutingHelper
|
||||
|
||||
attributes :name, :url
|
||||
|
||||
def url
|
||||
tag_url(object)
|
||||
end
|
||||
end
|
||||
class TagSerializer < REST::ShallowTagSerializer; end
|
||||
end
|
||||
|
||||
@@ -135,6 +135,9 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
@account.discoverable = @json['discoverable'] || false
|
||||
@account.indexable = @json['indexable'] || false
|
||||
@account.memorial = @json['memorial'] || false
|
||||
@account.show_featured = @json['showFeatured'] if @json.key?('showFeatured')
|
||||
@account.show_media = @json['showMedia'] if @json.key?('showMedia')
|
||||
@account.show_media_replies = @json['showRepliesInMedia'] if @json.key?('showRepliesInMedia')
|
||||
@account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) }
|
||||
end
|
||||
|
||||
@@ -238,7 +241,7 @@ class ActivityPub::ProcessAccountService < BaseService
|
||||
url = first_of_value(value['url'])
|
||||
url = url['href'] if url.is_a?(Hash)
|
||||
description = value['summary'].presence || value['name'].presence
|
||||
description = description.strip[0...MediaAttachment::MAX_DESCRIPTION_LENGTH] if description.present?
|
||||
description = description.strip[0...MediaAttachment::MAX_DESCRIPTION_HARD_LENGTH_LIMIT] if description.present?
|
||||
else
|
||||
url = value
|
||||
end
|
||||
|
||||
@@ -14,21 +14,22 @@ class ActivityPub::ProcessFeaturedCollectionService
|
||||
return if non_matching_uri_hosts?(@account.uri, @json['id'])
|
||||
|
||||
with_redis_lock("collection:#{@json['id']}") do
|
||||
return if @account.collections.exists?(uri: @json['id'])
|
||||
Collection.transaction do
|
||||
@collection = @account.collections.find_or_initialize_by(uri: @json['id'])
|
||||
|
||||
@collection = @account.collections.create!(
|
||||
local: false,
|
||||
uri: @json['id'],
|
||||
name: (@json['name'] || '')[0, Collection::NAME_LENGTH_HARD_LIMIT],
|
||||
description_html: truncated_summary,
|
||||
language:,
|
||||
sensitive: @json['sensitive'],
|
||||
discoverable: @json['discoverable'],
|
||||
original_number_of_items: @json['totalItems'] || 0,
|
||||
tag_name: @json.dig('topic', 'name')
|
||||
)
|
||||
@collection.update!(
|
||||
local: false,
|
||||
name: (@json['name'] || '')[0, Collection::NAME_LENGTH_HARD_LIMIT],
|
||||
description_html: truncated_summary,
|
||||
language:,
|
||||
sensitive: @json['sensitive'],
|
||||
discoverable: @json['discoverable'],
|
||||
original_number_of_items: @json['totalItems'] || 0,
|
||||
tag_name: @json.dig('topic', 'name')
|
||||
)
|
||||
|
||||
process_items! if @json['totalItems'].positive?
|
||||
process_items!
|
||||
end
|
||||
|
||||
@collection
|
||||
end
|
||||
@@ -46,8 +47,13 @@ class ActivityPub::ProcessFeaturedCollectionService
|
||||
end
|
||||
|
||||
def process_items!
|
||||
@json['orderedItems'].take(ITEMS_LIMIT).each do |item_json|
|
||||
ActivityPub::ProcessFeaturedItemWorker.perform_async(@collection.id, item_json, @request_id)
|
||||
uris = []
|
||||
items = @json['orderedItems'] || []
|
||||
items.take(ITEMS_LIMIT).each_with_index do |item_json, index|
|
||||
uris << value_or_id(item_json)
|
||||
ActivityPub::ProcessFeaturedItemWorker.perform_async(@collection.id, item_json, index, @request_id)
|
||||
end
|
||||
uris.compact!
|
||||
@collection.collection_items.where.not(uri: uris).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,29 +5,24 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
include Lockable
|
||||
include Redisable
|
||||
|
||||
def call(collection, uri_or_object, request_id: nil)
|
||||
def call(collection, uri_or_object, position: nil, request_id: nil)
|
||||
@collection = collection
|
||||
@request_id = request_id
|
||||
item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object
|
||||
return if non_matching_uri_hosts?(collection.uri, item_json['id'])
|
||||
@item_json = uri_or_object.is_a?(String) ? fetch_resource(uri_or_object, true) : uri_or_object
|
||||
return if non_matching_uri_hosts?(@collection.uri, @item_json['id'])
|
||||
|
||||
with_redis_lock("collection_item:#{item_json['id']}") do
|
||||
return if collection.collection_items.exists?(uri: item_json['id'])
|
||||
with_redis_lock("collection_item:#{@item_json['id']}") do
|
||||
@collection_item = existing_item || pre_approved_item || new_item
|
||||
|
||||
local_account = ActivityPub::TagManager.instance.uris_to_local_accounts([item_json['featuredObject']]).first
|
||||
@collection_item.update!(
|
||||
uri: @item_json['id'],
|
||||
object_uri: value_or_id(@item_json['featuredObject']),
|
||||
position:
|
||||
)
|
||||
|
||||
if local_account.present?
|
||||
# This is a local account that has authorized this item already
|
||||
@collection_item = collection.collection_items.accepted_partial(local_account).first
|
||||
@collection_item&.update!(uri: item_json['id'])
|
||||
else
|
||||
@collection_item = collection.collection_items.create!(
|
||||
uri: item_json['id'],
|
||||
object_uri: item_json['featuredObject']
|
||||
)
|
||||
@approval_uri = item_json['featureAuthorization']
|
||||
@approval_uri = @item_json['featureAuthorization']
|
||||
|
||||
verify_authorization!
|
||||
end
|
||||
verify_authorization! unless @collection_item&.account&.local?
|
||||
|
||||
@collection_item
|
||||
end
|
||||
@@ -35,6 +30,20 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
|
||||
private
|
||||
|
||||
def existing_item
|
||||
@collection.collection_items.find_by(uri: @item_json['id'])
|
||||
end
|
||||
|
||||
def pre_approved_item
|
||||
# This is a local account that has authorized this item already
|
||||
local_account = ActivityPub::TagManager.instance.uris_to_local_accounts([@item_json['featuredObject']]).first
|
||||
@collection.collection_items.accepted_partial(local_account).first if local_account.present?
|
||||
end
|
||||
|
||||
def new_item
|
||||
@collection.collection_items.new
|
||||
end
|
||||
|
||||
def verify_authorization!
|
||||
ActivityPub::VerifyFeaturedItemService.new.call(@collection_item, @approval_uri, request_id: @request_id)
|
||||
rescue Mastodon::RecursionLimitExceededError, Mastodon::UnexpectedResponseError, *Mastodon::HTTP_CONNECTION_ERRORS
|
||||
|
||||
@@ -182,9 +182,10 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
|
||||
def update_metadata!
|
||||
@raw_tags = []
|
||||
@raw_tags = []
|
||||
@raw_mentions = []
|
||||
@raw_emojis = []
|
||||
@raw_tagged_objects = []
|
||||
@raw_emojis = []
|
||||
|
||||
as_array(@json['tag']).each do |tag|
|
||||
if equals_or_includes?(tag['type'], 'Hashtag')
|
||||
@@ -193,10 +194,13 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
@raw_mentions << tag['href'] if tag['href'].present?
|
||||
elsif equals_or_includes?(tag['type'], 'Emoji')
|
||||
@raw_emojis << tag
|
||||
elsif equals_or_includes?(tag['type'], 'FeaturedCollection')
|
||||
@raw_tagged_objects << tag if tag['id']
|
||||
end
|
||||
end
|
||||
|
||||
update_tags!
|
||||
update_tagged_objects!
|
||||
update_mentions!
|
||||
update_emojis!
|
||||
update_quote!
|
||||
@@ -229,6 +233,24 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
end
|
||||
|
||||
def update_tagged_objects!
|
||||
current_tagged_objects = @raw_tagged_objects.filter_map do |tagged_object|
|
||||
url = tagged_object['id']
|
||||
|
||||
# TODO: We probably want to resolve unknown objects at authoring time
|
||||
ActivityPub::TagManager.instance.uri_to_resource(url, Collection)
|
||||
end
|
||||
|
||||
# Any previously-unresolved URI would be resolved here
|
||||
@status.tagged_objects.upsert_all(
|
||||
current_tagged_objects.uniq.map { |object| { object_type: object.class.name, object_id: object.id, uri: ActivityPub::TagManager.instance.uri_for(object), ap_type: 'FeaturedCollection' } },
|
||||
unique_by: %w(status_id uri)
|
||||
)
|
||||
|
||||
# Remove unused links
|
||||
@status.tagged_objects.where.not(uri: current_tagged_objects.map { |object| ActivityPub::TagManager.instance.uri_for(object) }).delete_all
|
||||
end
|
||||
|
||||
def update_mentions!
|
||||
unresolved_mentions = []
|
||||
|
||||
|
||||
@@ -93,9 +93,10 @@ class PostStatusService < BaseService
|
||||
|
||||
def process_status!
|
||||
@status = @account.statuses.new(status_attributes)
|
||||
process_mentions_service.call(@status, save_records: false)
|
||||
process_mentions_service.call(@status)
|
||||
safeguard_mentions!(@status)
|
||||
safeguard_private_mention_quote!(@status)
|
||||
attach_tagged_objects!(@status)
|
||||
attach_quote!(@status)
|
||||
|
||||
antispam = Antispam.new(@status)
|
||||
@@ -127,6 +128,10 @@ class PostStatusService < BaseService
|
||||
status.quote.accept! if @quoted_status.local? && StatusPolicy.new(@status.account, @quoted_status).quote?
|
||||
end
|
||||
|
||||
def attach_tagged_objects!(status)
|
||||
ProcessLinksService.new.call(status)
|
||||
end
|
||||
|
||||
def safeguard_mentions!(status)
|
||||
return if @options[:allowed_mentions].nil?
|
||||
|
||||
|
||||
53
app/services/process_links_service.rb
Normal file
53
app/services/process_links_service.rb
Normal file
@@ -0,0 +1,53 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class ProcessLinksService < BaseService
|
||||
include Payloadable
|
||||
|
||||
# Scan status for links to ActivityPub objects and attach them to statuses
|
||||
# @param [Status] status
|
||||
def call(status)
|
||||
return unless status.local?
|
||||
|
||||
@status = status
|
||||
@previous_objects = @status.tagged_objects.includes(:object).to_a
|
||||
@current_objects = []
|
||||
|
||||
Status.transaction do
|
||||
scan_text!
|
||||
assign_tagged_objects!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scan_text!
|
||||
urls = @status.text.scan(FetchLinkCardService::URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
|
||||
|
||||
urls.each do |url|
|
||||
# We only support `FeaturedCollection` at this time
|
||||
|
||||
# TODO: We probably want to resolve unknown objects at authoring time
|
||||
object = ActivityPub::TagManager.instance.uri_to_resource(url.to_s, Collection)
|
||||
next if object.nil?
|
||||
|
||||
tagged_object = @previous_objects.find { |x| x.object == object || x.uri == url }
|
||||
tagged_object ||= @current_objects.find { |x| x.object == object || x.uri == url }
|
||||
tagged_object ||= @status.tagged_objects.new(object: object, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(object))
|
||||
|
||||
@current_objects << tagged_object
|
||||
end
|
||||
end
|
||||
|
||||
def assign_tagged_objects!
|
||||
return unless @status.persisted?
|
||||
|
||||
@current_objects.each do |object|
|
||||
object.save if object.new_record?
|
||||
end
|
||||
|
||||
# If previous objects are no longer contained in the text, remove them to lighten the database
|
||||
removed_objects = @previous_objects - @current_objects
|
||||
|
||||
TaggedObject.where(id: removed_objects.map(&:id)).delete_all unless removed_objects.empty?
|
||||
end
|
||||
end
|
||||
@@ -6,10 +6,8 @@ class ProcessMentionsService < BaseService
|
||||
# Scan status for mentions and fetch remote mentioned users,
|
||||
# and create local mention pointers
|
||||
# @param [Status] status
|
||||
# @param [Boolean] save_records Whether to save records in database
|
||||
def call(status, save_records: true)
|
||||
def call(status)
|
||||
@status = status
|
||||
@save_records = save_records
|
||||
|
||||
return unless @status.local?
|
||||
|
||||
@@ -64,7 +62,7 @@ class ProcessMentionsService < BaseService
|
||||
"@#{mentioned_account.acct}"
|
||||
end
|
||||
|
||||
@status.save! if @save_records
|
||||
@status.save! if @status.persisted?
|
||||
end
|
||||
|
||||
def assign_mentions!
|
||||
@@ -79,8 +77,10 @@ class ProcessMentionsService < BaseService
|
||||
dropped_mentions.each(&:destroy)
|
||||
end
|
||||
|
||||
return unless @status.persisted?
|
||||
|
||||
@current_mentions.each do |mention|
|
||||
mention.save if (mention.new_record? || mention.silent_changed?) && @save_records
|
||||
mention.save if mention.new_record? || mention.silent_changed?
|
||||
end
|
||||
|
||||
# If previous mentions are no longer contained in the text, convert them
|
||||
|
||||
@@ -136,6 +136,7 @@ class UpdateStatusService < BaseService
|
||||
def update_metadata!
|
||||
ProcessHashtagsService.new.call(@status)
|
||||
ProcessMentionsService.new.call(@status)
|
||||
ProcessLinksService.new.call(@status)
|
||||
end
|
||||
|
||||
def broadcast_updates!
|
||||
|
||||
@@ -6,10 +6,10 @@ class ActivityPub::ProcessFeaturedItemWorker
|
||||
|
||||
sidekiq_options queue: 'pull', retry: 3
|
||||
|
||||
def perform(collection_id, id_or_json, request_id = nil)
|
||||
def perform(collection_id, id_or_json, position = nil, request_id = nil)
|
||||
collection = Collection.find(collection_id)
|
||||
|
||||
ActivityPub::ProcessFeaturedItemService.new.call(collection, id_or_json, request_id:)
|
||||
ActivityPub::ProcessFeaturedItemService.new.call(collection, id_or_json, position:, request_id:)
|
||||
rescue ActiveRecord::RecordNotFound
|
||||
true
|
||||
end
|
||||
|
||||
@@ -9,6 +9,7 @@ class PollExpirationNotifyWorker
|
||||
@poll = Poll.find(poll_id)
|
||||
|
||||
return if missing_expiration?
|
||||
|
||||
requeue! && return if not_due_yet?
|
||||
|
||||
notify_remote_voters_and_owner! if @poll.local?
|
||||
|
||||
@@ -361,7 +361,7 @@ es-MX:
|
||||
copy_failed_msg: No se pudo realizar una copia local de ese emoji
|
||||
create_new_category: Crear una nueva categoría
|
||||
created_msg: "¡Emoji creado con éxito!"
|
||||
delete: Borrar
|
||||
delete: Eliminar
|
||||
destroyed_msg: "¡Emojo destruido con éxito!"
|
||||
disable: Deshabilitar
|
||||
disabled: Desactivado
|
||||
@@ -473,7 +473,7 @@ es-MX:
|
||||
one: "%{count} intentos durante la última semana"
|
||||
other: "%{count} intentos de registro en la última semana"
|
||||
created_msg: Dominio de correo bloqueado con éxito
|
||||
delete: Borrar
|
||||
delete: Eliminar
|
||||
dns:
|
||||
types:
|
||||
mx: Registro MX
|
||||
@@ -610,7 +610,7 @@ es-MX:
|
||||
private_comment: Comentario privado
|
||||
public_comment: Comentario público
|
||||
purge: Purgar
|
||||
purge_description_html: Si crees que este dominio está desconectado, puedes borrar todos los registros de cuentas y los datos asociados de este dominio de tu almacenamiento. Esto puede llevar un tiempo.
|
||||
purge_description_html: Si crees que este dominio ya no está activo, puedes eliminar de tu almacenamiento todos los registros de la cuenta y los datos asociados a este dominio. Esto puede tardar un rato.
|
||||
title: Instancias conocidas
|
||||
total_blocked_by_us: Bloqueado por nosotros
|
||||
total_followed_by_them: Seguidos por ellos
|
||||
@@ -646,7 +646,7 @@ es-MX:
|
||||
title: Relaciones de %{acct}
|
||||
relays:
|
||||
add_new: Añadir nuevo relé
|
||||
delete: Borrar
|
||||
delete: Eliminar
|
||||
description_html: Un <strong>relés de federación</strong> es un servidor intermedio que intercambia grandes volúmenes de publicaciones públicas entre servidores que se suscriben y publican en él. <strong>Puede ayudar a servidores pequeños y medianos a descubrir contenido del fediverso</strong>, que de otra manera requeriría que los usuarios locales siguiesen manualmente a personas de servidores remotos.
|
||||
disable: Deshabilitar
|
||||
disabled: Deshabilitado
|
||||
@@ -776,7 +776,7 @@ es-MX:
|
||||
privileges:
|
||||
administrator: Administrador
|
||||
administrator_description: Los usuarios con este permiso saltarán todos los permisos
|
||||
delete_user_data: Borrar Datos de Usuario
|
||||
delete_user_data: Eliminar datos de usuario
|
||||
delete_user_data_description: Permite a los usuarios eliminar los datos de otros usuarios sin demora
|
||||
invite_bypass_approval: Invitar a usuarios sin revisión
|
||||
invite_bypass_approval_description: Permite que las personas invitadas al servidor por estos usuarios no tengan que pasar por el proceso de aprobación de la moderación
|
||||
@@ -1148,7 +1148,7 @@ es-MX:
|
||||
updated_msg: Regla de nombre de usuario actualizada correctamente
|
||||
warning_presets:
|
||||
add_new: Añadir nuevo
|
||||
delete: Borrar
|
||||
delete: Eliminar
|
||||
edit_preset: Editar aviso predeterminado
|
||||
empty: Aún no has definido ningún preajuste de advertencia.
|
||||
title: Preajustes de advertencia
|
||||
@@ -1260,7 +1260,7 @@ es-MX:
|
||||
registration_complete: "¡Tu registro en %{domain} ha sido completado!"
|
||||
welcome_title: "¡Bienvenido, %{name}!"
|
||||
wrong_email_hint: Si esa dirección de correo electrónico no es correcta, puedes cambiarla en la configuración de la cuenta.
|
||||
delete_account: Borrar cuenta
|
||||
delete_account: Eliminar cuenta
|
||||
delete_account_html: Si deseas eliminar tu cuenta, puedes <a href="%{path}">proceder aquí</a>. Se te pedirá una confirmación.
|
||||
description:
|
||||
prefix_invited_by_user: "¡@%{name} te invita a unirte a este servidor de Mastodon!"
|
||||
@@ -1457,10 +1457,10 @@ es-MX:
|
||||
mutes: Tienes en silencio
|
||||
storage: Almacenamiento
|
||||
featured_tags:
|
||||
add_new: Añadir nuevo
|
||||
add_new: Añadir nueva
|
||||
errors:
|
||||
limit: Ya has alcanzado la cantidad máxima de etiquetas
|
||||
hint_html: "<strong>¿Qué son las etiquetas destacadas?</strong> Se muestran de forma prominente en tu perfil público y permiten a los usuarios navegar por tus publicaciones públicas específicamente bajo esas etiquetas. Son una gran herramienta para hacer un seguimiento de trabajos creativos o proyectos a largo plazo."
|
||||
hint_html: "<strong>Destaca tus etiquetas más importantes en tu perfil.</strong> Una herramienta fantástica para llevar un registro de tus trabajos creativos y proyectos a largo plazo; las etiquetas destacadas aparecen en un lugar visible de tu perfil y te permiten acceder rápidamente a tus propias publicaciones."
|
||||
filters:
|
||||
contexts:
|
||||
account: Perfiles
|
||||
@@ -1479,7 +1479,7 @@ es-MX:
|
||||
invalid_context: Se suminstró un contexto inválido o vacío
|
||||
index:
|
||||
contexts: Filtros en %{contexts}
|
||||
delete: Borrar
|
||||
delete: Eliminar
|
||||
empty: No tienes filtros.
|
||||
expires_in: Caduca en %{distance}
|
||||
expires_on: Expira el %{date}
|
||||
@@ -1913,7 +1913,7 @@ es-MX:
|
||||
appearance: Apariencia
|
||||
authorized_apps: Aplicaciones autorizadas
|
||||
back: Volver al inicio
|
||||
delete: Borrar cuenta
|
||||
delete: Eliminar cuenta
|
||||
development: Desarrollo
|
||||
edit_profile: Editar perfil
|
||||
export: Exportar
|
||||
@@ -1991,7 +1991,7 @@ es-MX:
|
||||
unlisted: Pública, pero silenciosa
|
||||
unlisted_long: Ocultado de los resultados de búsqueda, tendencias y cronologías públicas de Mastodon
|
||||
statuses_cleanup:
|
||||
enabled: Borrar automáticamente publicaciones antiguas
|
||||
enabled: Eliminar automáticamente las publicaciones antiguas
|
||||
enabled_hint: Elimina automáticamente tus publicaciones una vez que alcancen un umbral de tiempo especificado, a menos que coincidan con alguna de las excepciones detalladas debajo
|
||||
exceptions: Excepciones
|
||||
explanation: La eliminación automática se realiza con baja prioridad. Puede haber un retraso entre el momento en que se alcanza el límite de antigüedad y el momento en que se elimina.
|
||||
|
||||
@@ -778,6 +778,7 @@ fi:
|
||||
administrator_description: Käyttäjät, joilla on tämä käyttöoikeus, ohittavat jokaisen käyttöoikeuden
|
||||
delete_user_data: Poistaa käyttäjän tiedot
|
||||
delete_user_data_description: Sallii käyttäjien poistaa muiden käyttäjien tiedot viipymättä
|
||||
invite_bypass_approval: Kutsu käyttäjiä arvioimatta
|
||||
invite_users: Kutsua käyttäjiä
|
||||
invite_users_description: Sallii käyttäjien kutsua uusia käyttäjiä palvelimelle
|
||||
manage_announcements: Hallita tiedotteita
|
||||
|
||||
@@ -781,6 +781,8 @@ fr-CA:
|
||||
administrator_description: Les utilisateur⋅rice⋅s ayant cette autorisation pourront contourner toutes les autorisations
|
||||
delete_user_data: Supprimer les données de l'utilisateur⋅rice
|
||||
delete_user_data_description: Permet aux utilisateur⋅rice⋅s de supprimer sans délai les données des autres utilisateur⋅rice⋅s
|
||||
invite_bypass_approval: Inviter des utilisateurs sans vérifications
|
||||
invite_bypass_approval_description: Permet aux personnes invitées sur le serveur par ces utilisateur·rice·s de s'inscrire sans être soumises à l'approbation de la modération
|
||||
invite_users: Inviter des utilisateur⋅rice⋅s
|
||||
invite_users_description: Permet aux utilisateur⋅rice⋅s d'inviter de nouvelles personnes sur le serveur
|
||||
manage_announcements: Gérer les annonces
|
||||
|
||||
@@ -781,6 +781,8 @@ fr:
|
||||
administrator_description: Les utilisateur⋅rice⋅s ayant cette autorisation pourront contourner toutes les autorisations
|
||||
delete_user_data: Supprimer les données de l'utilisateur⋅rice
|
||||
delete_user_data_description: Permet aux utilisateur⋅rice⋅s de supprimer sans délai les données des autres utilisateur⋅rice⋅s
|
||||
invite_bypass_approval: Inviter des utilisateurs sans vérifications
|
||||
invite_bypass_approval_description: Permet aux personnes invitées sur le serveur par ces utilisateur·rice·s de s'inscrire sans être soumises à l'approbation de la modération
|
||||
invite_users: Inviter des utilisateur⋅rice⋅s
|
||||
invite_users_description: Permet aux utilisateur⋅rice⋅s d'inviter de nouvelles personnes sur le serveur
|
||||
manage_announcements: Gérer les annonces
|
||||
|
||||
@@ -762,6 +762,8 @@ nan-TW:
|
||||
administrator_description: 有tsit ê權限ê用者ē忽略所有ê權限。
|
||||
delete_user_data: Thâi掉用者ê資料
|
||||
delete_user_data_description: 允准用者liâm-mi thâi掉其他用者ê資料
|
||||
invite_bypass_approval: 邀請iáu bē審查ê用者
|
||||
invite_bypass_approval_description: 允准予tsiah ê用者邀請kàu tsit臺服侍器ê lâng làng過管理審查許可
|
||||
invite_users: 邀請用者
|
||||
invite_users_description: 允准用者邀請新lâng來tsit ê服侍器
|
||||
manage_announcements: 管理公告
|
||||
@@ -1238,7 +1240,7 @@ nan-TW:
|
||||
welcome_title: 歡迎 %{name}!
|
||||
wrong_email_hint: Nā是電子phue ê地址無正確,lí ē當tī口座設定kā改。
|
||||
delete_account: Thâi掉口座
|
||||
delete_account_html: Nā lí behthâi掉lí ê口座,lí ē當<a href="%{path}">ji̍h tsia繼續</a>。Lí著確認動作。
|
||||
delete_account_html: Nā lí beh thâi掉lí ê口座,lí ē當<a href="%{path}">ji̍h tsia繼續</a>。Lí著確認動作。
|
||||
description:
|
||||
prefix_invited_by_user: "@%{name} 邀請lí加入tsit ê Mastodon 服侍器!"
|
||||
prefix_sign_up: Tsit-má註冊Mastodon ê口座!
|
||||
@@ -1258,6 +1260,7 @@ nan-TW:
|
||||
progress:
|
||||
confirm: 確認電子phue
|
||||
details: Lí ê詳細
|
||||
list: 註冊流程
|
||||
review: Lán ê審查
|
||||
rules: 接受規則
|
||||
providers:
|
||||
@@ -1273,6 +1276,7 @@ nan-TW:
|
||||
invited_by: Lí通用有tuì hia收著ê邀請加入 %{domain}:
|
||||
preamble: Tsiah-ê hōo %{domain} ê管理員設定kap實施。
|
||||
preamble_invited: 佇lí繼續進前,請思考 %{domain} ê管理員設立ê基本規則。
|
||||
read_more: 讀詳細
|
||||
title: Tsi̍t-kuá基本規定。
|
||||
title_invited: Lí受邀請ah。
|
||||
security: 安全
|
||||
@@ -1953,6 +1957,36 @@ nan-TW:
|
||||
keep_direct: 保留私人ê短phue
|
||||
keep_direct_hint: Bē thâi掉任何lí ê私人ê提起
|
||||
keep_media: 保留有媒體附件ê PO文
|
||||
keep_media_hint: Bē thâi掉lí ê任何有媒體附件ê PO文
|
||||
keep_pinned: 保留所釘ê PO文
|
||||
keep_pinned_hint: Bē thâi掉任何lí所釘ê PO文
|
||||
keep_polls: 保留投票
|
||||
keep_polls_hint: Bē thâi掉任何lí ê投票
|
||||
keep_self_bookmark: 保留lí加冊籤ê PO文
|
||||
keep_self_bookmark_hint: Bē thâi掉lí家kī有加冊籤ê PO文
|
||||
keep_self_fav: 保留lí收藏ê PO文
|
||||
keep_self_fav_hint: Bē thâi掉lí家kī有收藏ê PO文
|
||||
min_age:
|
||||
'1209600': 2 禮拜
|
||||
'15778476': 6 個月
|
||||
'2629746': 1 個月
|
||||
'31556952': 1 年
|
||||
'5259492': 2 個月
|
||||
'604800': 1 禮拜
|
||||
'63113904': 2 年
|
||||
'7889238': 3 個月
|
||||
min_age_label: 保持PO文ê期間
|
||||
min_favs: 保留超過下kha ê收藏數ê PO文
|
||||
min_favs_hint: 若是lí ê PO文有kàu tsit ê收藏數,就bē受thâi掉。留空白表示毋管收藏數,PO文lóng thâi掉
|
||||
min_reblogs: 保留超過下kha ê轉送數ê PO文
|
||||
min_reblogs_hint: 若是lí ê PO文有kàu tsit ê轉送數,就bē受thâi掉。留空白表示毋管轉送數,PO文lóng thâi掉
|
||||
stream_entries:
|
||||
sensitive_content: 敏感ê內容
|
||||
strikes:
|
||||
errors:
|
||||
too_late: Lí bē赴申訴tsit ê處份ah
|
||||
tags:
|
||||
does_not_match_previous_name: kap進前ê名無kâng
|
||||
terms_of_service:
|
||||
title: 服務規定
|
||||
themes:
|
||||
|
||||
@@ -778,6 +778,8 @@ nl:
|
||||
administrator_description: Deze gebruikers hebben volledige rechten en kun dus overal bij
|
||||
delete_user_data: Gebruikersgegevens verwijderen
|
||||
delete_user_data_description: Staat gebruikers toe om de gegevens van andere gebruikers zonder vertraging te verwijderen
|
||||
invite_bypass_approval: Gebruikers uitnodigen zonder beoordeling
|
||||
invite_bypass_approval_description: Staat het toe dat mensen die door deze gebruikers voor deze server zijn uitgenodigd, niet door moderatoren beoordeeld hoeven te worden
|
||||
invite_users: Gebruikers uitnodigen
|
||||
invite_users_description: Staat gebruikers toe om nieuwe mensen voor de server uit te nodigen
|
||||
manage_announcements: Aankondigingen beheren
|
||||
@@ -1279,7 +1281,7 @@ nl:
|
||||
progress:
|
||||
confirm: E-mailadres bevestigen
|
||||
details: Jouw gegevens
|
||||
list: Voortgang aanmelding
|
||||
list: Voortgang registratie
|
||||
review: Onze beoordeling
|
||||
rules: Regels accepteren
|
||||
providers:
|
||||
@@ -1295,6 +1297,7 @@ nl:
|
||||
invited_by: 'Je kunt je registreren op %{domain} dankzij de uitnodiging die je hebt ontvangen van:'
|
||||
preamble: Deze zijn vastgesteld en worden gehandhaafd door de moderatoren van %{domain}.
|
||||
preamble_invited: Voordat je verder gaat, moet je eerst kennisnemen van de basisregels die door de moderatoren van %{domain} zijn vastgesteld.
|
||||
read_more: Meer lezen
|
||||
title: Enkele basisregels.
|
||||
title_invited: Je bent uitgenodigd.
|
||||
security: Beveiliging
|
||||
|
||||
@@ -77,7 +77,7 @@ es-MX:
|
||||
domain: Este puede ser el nombre de dominio que se muestra en la dirección de correo o el registro MX que utiliza. Se comprobarán al registrarse.
|
||||
with_dns_records: Se hará un intento de resolver los registros DNS del dominio dado y los resultados serán también puestos en lista negra
|
||||
featured_tag:
|
||||
name: 'Aquí están algunas de las etiquetas que más has usado recientemente:'
|
||||
name: 'Estas son algunas de las etiquetas que más has utilizado recientemente:'
|
||||
filters:
|
||||
action: Elige qué acción realizar cuando una publicación coincida con el filtro
|
||||
actions:
|
||||
|
||||
@@ -778,6 +778,8 @@ tr:
|
||||
administrator_description: Bu izne sahip kullanıcılar tüm diğer izinleri atlıyorlar
|
||||
delete_user_data: Kullanıcı Verilerini Silme
|
||||
delete_user_data_description: Kullanıcıların, diğer kullanıcıların verisini gecikme olmaksızın silmesine izin verir
|
||||
invite_bypass_approval: Kullanıcıları incelemeden davet et
|
||||
invite_bypass_approval_description: Bu kullanıcılar tarafından sunucuya davet edilen kişilerin moderasyon onayını atlamasına izin verir
|
||||
invite_users: Kullanıcıları Davet Etme
|
||||
invite_users_description: Kullanıcıların yeni kişileri sunucuya davet etmesine izin verir
|
||||
manage_announcements: Duyuruları Yönetme
|
||||
|
||||
@@ -762,6 +762,8 @@ zh-CN:
|
||||
administrator_description: 拥有此权限的用户将绕过所有权限限制。
|
||||
delete_user_data: 删除用户数据
|
||||
delete_user_data_description: 允许用户立即删除其他用户的数据
|
||||
invite_bypass_approval: 邀请未经审核的用户
|
||||
invite_bypass_approval_description: 允许被这些用户邀请到此服务器的人们绕过管理审核批准
|
||||
invite_users: 邀请用户
|
||||
invite_users_description: 允许用户邀请新人加入站点
|
||||
manage_announcements: 管理公告
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'sidekiq_unique_jobs/web' if ENV['ENABLE_SIDEKIQ_UNIQUE_JOBS_UI'] == true
|
||||
require 'sidekiq-scheduler/web'
|
||||
|
||||
class RedirectWithVary < ActionDispatch::Routing::PathRedirect
|
||||
|
||||
@@ -19,7 +19,7 @@ export function GlitchThemes(): Plugin {
|
||||
return {
|
||||
name: 'glitch-themes',
|
||||
async config(userConfig) {
|
||||
const existingInputs = userConfig.build?.rollupOptions?.input;
|
||||
const existingInputs = userConfig.build?.rolldownOptions?.input;
|
||||
|
||||
if (typeof existingInputs === 'string') {
|
||||
entrypoints[path.basename(existingInputs)] = existingInputs;
|
||||
@@ -79,7 +79,7 @@ export function GlitchThemes(): Plugin {
|
||||
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
input: entrypoints,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@ export function MastodonThemes(): Plugin {
|
||||
|
||||
let entrypoints: Record<string, string> = {};
|
||||
|
||||
const existingInputs = userConfig.build?.rollupOptions?.input;
|
||||
const existingInputs = userConfig.build?.rolldownOptions?.input;
|
||||
|
||||
if (typeof existingInputs === 'string') {
|
||||
entrypoints[path.basename(existingInputs)] = existingInputs;
|
||||
@@ -46,7 +46,7 @@ export function MastodonThemes(): Plugin {
|
||||
|
||||
return {
|
||||
build: {
|
||||
rollupOptions: {
|
||||
rolldownOptions: {
|
||||
input: entrypoints,
|
||||
},
|
||||
},
|
||||
|
||||
17
db/migrate/20260319142348_create_tagged_objects.rb
Normal file
17
db/migrate/20260319142348_create_tagged_objects.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CreateTaggedObjects < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
create_table :tagged_objects do |t|
|
||||
t.references :status, null: false, foreign_key: { on_delete: :cascade }, index: false
|
||||
t.references :object, polymorphic: true, null: true
|
||||
t.string :ap_type, null: false
|
||||
t.string :uri
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :tagged_objects, [:status_id, :object_type, :object_id], unique: true, where: 'object_type IS NOT NULL AND object_id IS NOT NULL'
|
||||
add_index :tagged_objects, [:status_id, :uri], unique: true, where: 'uri IS NOT NULL'
|
||||
end
|
||||
end
|
||||
16
db/schema.rb
16
db/schema.rb
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||
ActiveRecord::Schema[8.1].define(version: 2026_03_19_142348) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pg_catalog.plpgsql"
|
||||
|
||||
@@ -1245,6 +1245,19 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||
t.index ["tag_id", "language"], name: "index_tag_trends_on_tag_id_and_language", unique: true
|
||||
end
|
||||
|
||||
create_table "tagged_objects", force: :cascade do |t|
|
||||
t.string "ap_type", null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "object_id"
|
||||
t.string "object_type"
|
||||
t.bigint "status_id", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.string "uri"
|
||||
t.index ["object_type", "object_id"], name: "index_tagged_objects_on_object"
|
||||
t.index ["status_id", "object_type", "object_id"], name: "idx_on_status_id_object_type_object_id_d6ebe374bd", unique: true, where: "((object_type IS NOT NULL) AND (object_id IS NOT NULL))"
|
||||
t.index ["status_id", "uri"], name: "index_tagged_objects_on_status_id_and_uri", unique: true, where: "(uri IS NOT NULL)"
|
||||
end
|
||||
|
||||
create_table "tags", force: :cascade do |t|
|
||||
t.datetime "created_at", precision: nil, null: false
|
||||
t.string "display_name"
|
||||
@@ -1550,6 +1563,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_03_18_144837) do
|
||||
add_foreign_key "tag_follows", "accounts", on_delete: :cascade
|
||||
add_foreign_key "tag_follows", "tags", on_delete: :cascade
|
||||
add_foreign_key "tag_trends", "tags", on_delete: :cascade
|
||||
add_foreign_key "tagged_objects", "statuses", on_delete: :cascade
|
||||
add_foreign_key "tombstones", "accounts", on_delete: :cascade
|
||||
add_foreign_key "user_invite_requests", "users", on_delete: :cascade
|
||||
add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade
|
||||
|
||||
@@ -17,7 +17,6 @@ module Mastodon::CLI
|
||||
LONG_DESC
|
||||
def rotate(username = nil)
|
||||
if options[:all]
|
||||
processed = 0
|
||||
delay = 0
|
||||
scope = Account.local.without_suspended
|
||||
progress = create_progress_bar(scope.count)
|
||||
@@ -26,14 +25,13 @@ module Mastodon::CLI
|
||||
accounts.each do |account|
|
||||
rotate_keys_for_account(account, delay)
|
||||
progress.increment
|
||||
processed += 1
|
||||
end
|
||||
|
||||
delay += 5.minutes
|
||||
end
|
||||
|
||||
progress.finish
|
||||
say("OK, rotated keys for #{processed} accounts", :green)
|
||||
say("OK, rotated keys for #{progress.progress} accounts", :green)
|
||||
elsif username.present?
|
||||
rotate_keys_for_account(Account.find_local(username))
|
||||
say('OK', :green)
|
||||
@@ -442,7 +440,6 @@ module Mastodon::CLI
|
||||
total += account.following.reorder(nil).count if options[:follows]
|
||||
total += account.followers.reorder(nil).count if options[:followers]
|
||||
progress = create_progress_bar(total)
|
||||
processed = 0
|
||||
|
||||
if options[:follows]
|
||||
account.following.reorder(nil).find_each do |target_account|
|
||||
@@ -451,7 +448,6 @@ module Mastodon::CLI
|
||||
progress.log pastel.red("Error processing #{target_account.id}: #{e}")
|
||||
ensure
|
||||
progress.increment
|
||||
processed += 1
|
||||
end
|
||||
|
||||
BootstrapTimelineWorker.perform_async(account.id)
|
||||
@@ -464,12 +460,11 @@ module Mastodon::CLI
|
||||
progress.log pastel.red("Error processing #{target_account.id}: #{e}")
|
||||
ensure
|
||||
progress.increment
|
||||
processed += 1
|
||||
end
|
||||
end
|
||||
|
||||
progress.finish
|
||||
say("Processed #{processed} relationships", :green, true)
|
||||
say("Processed #{progress.progress} relationships", :green, true)
|
||||
end
|
||||
|
||||
option :number, type: :numeric, aliases: [:n]
|
||||
|
||||
36
package.json
36
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@mastodon/mastodon",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"packageManager": "yarn@4.12.0",
|
||||
"packageManager": "yarn@4.13.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -51,8 +51,9 @@
|
||||
"@optimize-lodash/rollup-plugin": "^6.0.0",
|
||||
"@react-spring/web": "^9.7.5",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@rolldown/plugin-babel": "^0.2.2",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"@vitejs/plugin-legacy": "^7.2.1",
|
||||
"@vitejs/plugin-legacy": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"arrow-key-navigation": "^1.2.0",
|
||||
"async-mutex": "^0.5.0",
|
||||
@@ -110,8 +111,8 @@
|
||||
"redux-immutable": "^4.0.0",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
"requestidlecallback": "^0.3.0",
|
||||
"rollup-plugin-gzip": "^4.1.1",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"rollup-plugin-gzip": "^4.2.0",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"sass": "^1.62.1",
|
||||
"scroll-behavior": "^0.11.0",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
@@ -121,11 +122,10 @@
|
||||
"tiny-queue": "^0.2.1",
|
||||
"twitter-text": "3.1.0",
|
||||
"use-debounce": "^10.0.0",
|
||||
"vite": "^7.1.1",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-manifest-sri": "^0.2.0",
|
||||
"vite-plugin-pwa": "^1.0.2",
|
||||
"vite-plugin-svgr": "^4.3.0",
|
||||
"vite-tsconfig-paths": "^6.0.0",
|
||||
"vite-plugin-pwa": "^1.2.0",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"wicg-inert": "^3.1.2",
|
||||
"workbox-expiration": "^7.3.0",
|
||||
"workbox-routing": "^7.3.0",
|
||||
@@ -135,10 +135,10 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@formatjs/cli": "^6.1.1",
|
||||
"@storybook/addon-a11y": "^10.0.6",
|
||||
"@storybook/addon-docs": "^10.0.6",
|
||||
"@storybook/addon-vitest": "^10.0.6",
|
||||
"@storybook/react-vite": "^10.0.6",
|
||||
"@storybook/addon-a11y": "^10.3.0",
|
||||
"@storybook/addon-docs": "^10.3.0",
|
||||
"@storybook/addon-vitest": "^10.3.0",
|
||||
"@storybook/react-vite": "^10.3.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/debug": "^4",
|
||||
@@ -163,10 +163,10 @@
|
||||
"@types/react-toggle": "^4.0.3",
|
||||
"@types/redux-immutable": "^4.0.3",
|
||||
"@types/requestidlecallback": "^0.3.5",
|
||||
"@vitest/browser": "^4.0.5",
|
||||
"@vitest/browser-playwright": "^4.0.5",
|
||||
"@vitest/coverage-v8": "^4.0.5",
|
||||
"@vitest/ui": "^4.0.5",
|
||||
"@vitest/browser": "^4.1.0",
|
||||
"@vitest/browser-playwright": "^4.1.0",
|
||||
"@vitest/coverage-v8": "^4.1.0",
|
||||
"@vitest/ui": "^4.1.0",
|
||||
"chromatic": "^13.3.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-import-resolver-typescript": "^4.2.5",
|
||||
@@ -187,13 +187,13 @@
|
||||
"oxfmt": "^0.33.0",
|
||||
"playwright": "^1.57.0",
|
||||
"react-test-renderer": "^18.2.0",
|
||||
"storybook": "^10.0.5",
|
||||
"storybook": "^10.3.0",
|
||||
"stylelint": "^17.0.0",
|
||||
"stylelint-config-standard-scss": "^17.0.0",
|
||||
"typescript": "~5.9.0",
|
||||
"typescript-eslint": "^8.55.0",
|
||||
"typescript-plugin-css-modules": "^5.2.0",
|
||||
"vitest": "^4.0.5"
|
||||
"vitest": "^4.1.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.2.7",
|
||||
|
||||
8
spec/fabricators/tagged_object_fabricator.rb
Normal file
8
spec/fabricators/tagged_object_fabricator.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Fabricator(:tagged_object) do
|
||||
status
|
||||
object nil
|
||||
ap_type 'FeaturedCollection'
|
||||
uri { Faker::Internet.device_token }
|
||||
end
|
||||
@@ -731,6 +731,30 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with tagged Featured Collections' do
|
||||
let(:featured_collection) { Fabricate(:collection) }
|
||||
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
tag: [
|
||||
{
|
||||
type: 'FeaturedCollection',
|
||||
id: ActivityPub::TagManager.instance.uri_for(featured_collection),
|
||||
},
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates the status with appropriate tagged objects' do
|
||||
expect { subject.perform }
|
||||
.to change(sender.statuses, :count).by(1)
|
||||
|
||||
status = sender.statuses.first
|
||||
|
||||
expect(status.tagged_objects.map(&:object)).to contain_exactly(featured_collection)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hashtags' do
|
||||
let(:object_json) do
|
||||
build_object(
|
||||
|
||||
@@ -256,5 +256,45 @@ RSpec.describe ActivityPub::Activity::Update do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a `FeaturedCollection` object', feature: :collections_federation do
|
||||
let(:collection) { Fabricate(:remote_collection, account: sender, name: 'old name', discoverable: false) }
|
||||
let(:featured_collection_json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => collection.uri,
|
||||
'type' => 'FeaturedCollection',
|
||||
'attributedTo' => sender.uri,
|
||||
'name' => 'Cool people',
|
||||
'summary' => 'People you should follow.',
|
||||
'totalItems' => 0,
|
||||
'sensitive' => false,
|
||||
'discoverable' => true,
|
||||
'published' => '2026-03-09T15:19:25Z',
|
||||
'updated' => Time.zone.now.iso8601,
|
||||
}
|
||||
end
|
||||
let(:json) do
|
||||
{
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Update',
|
||||
'actor' => sender.uri,
|
||||
'object' => featured_collection_json,
|
||||
}
|
||||
end
|
||||
let(:stubbed_service) do
|
||||
instance_double(ActivityPub::ProcessFeaturedCollectionService, call: true)
|
||||
end
|
||||
|
||||
before do
|
||||
allow(ActivityPub::ProcessFeaturedCollectionService).to receive(:new).and_return(stubbed_service)
|
||||
end
|
||||
|
||||
it 'updates the collection' do
|
||||
subject.perform
|
||||
|
||||
expect(stubbed_service).to have_received(:call).with(sender, featured_collection_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -671,5 +671,15 @@ RSpec.describe ActivityPub::TagManager do
|
||||
status = Fabricate(:status, uri: 'https://example.com/123')
|
||||
expect(subject.uri_to_resource('https://example.com/123#456', Status)).to eq status
|
||||
end
|
||||
|
||||
it 'returns the local featured collection' do
|
||||
collection = Fabricate(:collection)
|
||||
expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection
|
||||
end
|
||||
|
||||
it 'returns the remote featured collection' do
|
||||
collection = Fabricate(:remote_collection)
|
||||
expect(subject.uri_to_resource(subject.uri_for(collection), Collection)).to eq collection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,6 +43,26 @@ RSpec.describe ActivityPub::NoteSerializer do
|
||||
.and(not_include(reply_by_account_visibility_direct.uri)) # Replies with direct visibility
|
||||
end
|
||||
|
||||
context 'with tagged featured collections' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
|
||||
before do
|
||||
parent.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection))
|
||||
end
|
||||
|
||||
it 'has the expected shape' do
|
||||
expect(subject).to include({
|
||||
'type' => 'Note',
|
||||
'tag' => include(
|
||||
a_hash_including({
|
||||
'type' => 'FeaturedCollection',
|
||||
'id' => ActivityPub::TagManager.instance.uri_for(collection),
|
||||
})
|
||||
),
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a quote' do
|
||||
let(:quoted_status) { Fabricate(:status) }
|
||||
let!(:quote) { Fabricate(:quote, status: parent, quoted_status: quoted_status, state: :accepted) }
|
||||
|
||||
@@ -92,5 +92,22 @@ RSpec.describe REST::StatusSerializer do
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a tagged collection' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
|
||||
before do
|
||||
status.tagged_objects.create!(object: collection, ap_type: 'FeaturedCollection', uri: ActivityPub::TagManager.instance.uri_for(collection))
|
||||
end
|
||||
|
||||
it 'contains the tagged collection' do
|
||||
expect(subject)
|
||||
.to include(
|
||||
'tagged_collections' => [a_hash_including(
|
||||
'id' => collection.id.to_s
|
||||
)]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,6 +114,30 @@ RSpec.describe ActivityPub::ProcessAccountService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with profile settings' do
|
||||
let(:payload) do
|
||||
{
|
||||
id: 'https://foo.test',
|
||||
type: 'Actor',
|
||||
inbox: 'https://foo.test/inbox',
|
||||
showMedia: true,
|
||||
showRepliesInMedia: false,
|
||||
showFeatured: false,
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'sets the profile settings as expected' do
|
||||
account = subject.call('alice', 'example.com', payload)
|
||||
|
||||
expect(account)
|
||||
.to have_attributes(
|
||||
show_media: true,
|
||||
show_media_replies: false,
|
||||
show_featured: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with inlined feature collection' do
|
||||
let(:payload) do
|
||||
{
|
||||
|
||||
@@ -73,4 +73,36 @@ RSpec.describe ActivityPub::ProcessFeaturedCollectionService do
|
||||
expect(new_collection.description_html).to eq '<p>A list of remote actors you should follow.</p>'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the collection already exists' do
|
||||
let(:collection) { Fabricate(:remote_collection, account:, uri: base_json['id'], name: 'placeholder') }
|
||||
|
||||
before do
|
||||
Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/1')
|
||||
Fabricate(:collection_item, collection:, uri: 'https://example.com/featured_items/3')
|
||||
end
|
||||
|
||||
it 'updates the existing collection, removes the item that no longer exists and queues a jobs to fetch the other items' do
|
||||
expect { subject.call(account, featured_collection_json) }
|
||||
.to change(collection.collection_items, :count).by(-1)
|
||||
|
||||
expect(collection.reload.name).to eq 'Good people from other servers'
|
||||
expect(ActivityPub::ProcessFeaturedItemWorker).to have_enqueued_sidekiq_job.exactly(2).times
|
||||
end
|
||||
|
||||
context 'when the updated collection no longer contains any items' do
|
||||
let(:featured_collection_json) do
|
||||
base_json.merge({
|
||||
'summary' => summary,
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => nil,
|
||||
})
|
||||
end
|
||||
|
||||
it 'removes all items' do
|
||||
expect { subject.call(account, featured_collection_json) }
|
||||
.to change(collection.collection_items, :count).by(-2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,6 +8,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:collection) { Fabricate(:remote_collection, uri: 'https://other.example.com/collection/1') }
|
||||
let(:position) { 3 }
|
||||
let(:featured_object_uri) { 'https://example.com/actor/1' }
|
||||
let(:feature_authorization_uri) { 'https://example.com/auth/1' }
|
||||
let(:featured_item_json) do
|
||||
@@ -34,7 +35,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
|
||||
it 'does not create a collection item and returns `nil`' do
|
||||
expect do
|
||||
expect(subject.call(collection, object)).to be_nil
|
||||
expect(subject.call(collection, object, position:)).to be_nil
|
||||
end.to_not change(CollectionItem, :count)
|
||||
end
|
||||
end
|
||||
@@ -45,14 +46,29 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
|
||||
it_behaves_like 'non-matching URIs'
|
||||
|
||||
it 'creates and verifies the item' do
|
||||
expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1)
|
||||
context 'when item does not yet exist' do
|
||||
it 'creates and verifies the item' do
|
||||
expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1)
|
||||
|
||||
expect(stubbed_service).to have_received(:call)
|
||||
expect(stubbed_service).to have_received(:call)
|
||||
|
||||
new_item = collection.collection_items.last
|
||||
expect(new_item.object_uri).to eq 'https://example.com/actor/1'
|
||||
expect(new_item.approval_uri).to be_nil
|
||||
new_item = collection.collection_items.last
|
||||
expect(new_item.object_uri).to eq 'https://example.com/actor/1'
|
||||
expect(new_item.approval_uri).to be_nil
|
||||
expect(new_item.position).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context 'when item exists at a different position' do
|
||||
let!(:collection_item) do
|
||||
Fabricate(:collection_item, collection:, uri: featured_item_json['id'], position: 2)
|
||||
end
|
||||
|
||||
it 'updates the position' do
|
||||
expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count)
|
||||
|
||||
expect(collection_item.reload.position).to eq 3
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an item exists for a local featured account' do
|
||||
@@ -63,7 +79,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
let(:feature_authorization_uri) { ap_account_feature_authorization_url(collection_item.account_id, collection_item) }
|
||||
|
||||
it 'updates the URI of the existing record' do
|
||||
expect { subject.call(collection, object) }.to_not change(collection.collection_items, :count)
|
||||
expect { subject.call(collection, object, position:) }.to_not change(collection.collection_items, :count)
|
||||
expect(collection_item.reload.uri).to eq 'https://other.example.com/featured_item/1'
|
||||
end
|
||||
end
|
||||
@@ -87,7 +103,7 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
it_behaves_like 'non-matching URIs'
|
||||
|
||||
it 'fetches the collection item' do
|
||||
expect { subject.call(collection, object) }.to change(collection.collection_items, :count).by(1)
|
||||
expect { subject.call(collection, object, position:) }.to change(collection.collection_items, :count).by(1)
|
||||
|
||||
expect(featured_item_request).to have_been_requested
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user