Merge commit 'f69ca085dbfca2253404574dcdc4dc6c2aaa35c0' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-10-01 19:13:18 +02:00
147 changed files with 1188 additions and 808 deletions

View File

@@ -4,13 +4,13 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: :index
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only: :revoke
before_action :check_owner! before_action :set_statuses, only: :index
before_action :set_quote, only: :revoke before_action :set_quote, only: :revoke
after_action :insert_pagination_headers, only: :index after_action :insert_pagination_headers, only: :index
def index def index
cache_if_unauthenticated! cache_if_unauthenticated!
@statuses = load_statuses
render json: @statuses, each_serializer: REST::StatusSerializer render json: @statuses, each_serializer: REST::StatusSerializer
end end
@@ -24,18 +24,26 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
private private
def check_owner!
authorize @status, :list_quotes?
end
def set_quote def set_quote
@quote = @status.quotes.find_by!(status_id: params[:id]) @quote = @status.quotes.find_by!(status_id: params[:id])
end end
def load_statuses def set_statuses
scope = default_statuses scope = default_statuses
scope = scope.not_excluded_by_account(current_account) unless current_account.nil? scope = scope.not_excluded_by_account(current_account) unless current_account.nil?
scope.merge(paginated_quotes).to_a @statuses = scope.merge(paginated_quotes).to_a
# Store next page info before filtering
@records_continue = @statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
@pagination_since_id = @statuses.first.quote.id unless @statuses.empty?
@pagination_max_id = @statuses.last.quote.id if @records_continue
if current_account&.id != @status.account_id
domains = @statuses.filter_map(&:account_domain).uniq
account_ids = @statuses.map(&:account_id).uniq
relations = current_account&.relations_map(account_ids, domains) || {}
@statuses.reject! { |status| StatusFilter.new(status, current_account, relations).filtered? }
end
end end
def default_statuses def default_statuses
@@ -58,15 +66,9 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty? api_v1_status_quotes_url pagination_params(since_id: pagination_since_id) unless @statuses.empty?
end end
def pagination_max_id attr_reader :pagination_max_id, :pagination_since_id
@statuses.last.quote.id
end
def pagination_since_id
@statuses.first.quote.id
end
def records_continue? def records_continue?
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT) @records_continue
end end
end end

View File

@@ -1,11 +1,15 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import classNames from 'classnames';
import { useLinks } from 'mastodon/hooks/useLinks'; import { useLinks } from 'mastodon/hooks/useLinks';
import { EmojiHTML } from '../features/emoji/emoji_html';
import { useAppSelector } from '../store'; import { useAppSelector } from '../store';
import { isModernEmojiEnabled } from '../utils/environment'; import { isModernEmojiEnabled } from '../utils/environment';
import { AnimateEmojiProvider } from './emoji/context';
import { EmojiHTML } from './emoji/html';
interface AccountBioProps { interface AccountBioProps {
className: string; className: string;
accountId: string; accountId: string;
@@ -44,13 +48,13 @@ export const AccountBio: React.FC<AccountBioProps> = ({
} }
return ( return (
<div <AnimateEmojiProvider
className={`${className} translate`} className={classNames(className, 'translate')}
onClickCapture={handleClick} onClickCapture={handleClick}
ref={handleNodeChange} ref={handleNodeChange}
> >
<EmojiHTML htmlString={note} extraEmojis={extraEmojis} /> <EmojiHTML htmlString={note} extraEmojis={extraEmojis} />
</div> </AnimateEmojiProvider>
); );
}; };

View File

@@ -2,9 +2,10 @@ import type { ComponentPropsWithoutRef, FC } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { AnimateEmojiProvider } from '../emoji/context';
import { EmojiHTML } from '../emoji/html';
import { Skeleton } from '../skeleton'; import { Skeleton } from '../skeleton';
import type { DisplayNameProps } from './index'; import type { DisplayNameProps } from './index';
@@ -14,9 +15,10 @@ export const DisplayNameWithoutDomain: FC<
ComponentPropsWithoutRef<'span'> ComponentPropsWithoutRef<'span'>
> = ({ account, className, children, ...props }) => { > = ({ account, className, children, ...props }) => {
return ( return (
<span <AnimateEmojiProvider
{...props} {...props}
className={classNames('display-name animate-parent', className)} as='span'
className={classNames('display-name', className)}
> >
<bdi> <bdi>
{account ? ( {account ? (
@@ -27,8 +29,8 @@ export const DisplayNameWithoutDomain: FC<
? account.get('display_name') ? account.get('display_name')
: account.get('display_name_html') : account.get('display_name_html')
} }
shallow
as='strong' as='strong'
extraEmojis={account.get('emojis')}
/> />
) : ( ) : (
<strong className='display-name__html'> <strong className='display-name__html'>
@@ -37,6 +39,6 @@ export const DisplayNameWithoutDomain: FC<
)} )}
</bdi> </bdi>
{children} {children}
</span> </AnimateEmojiProvider>
); );
}; };

View File

@@ -1,8 +1,9 @@
import type { ComponentPropsWithoutRef, FC } from 'react'; import type { ComponentPropsWithoutRef, FC } from 'react';
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment'; import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { EmojiHTML } from '../emoji/html';
import type { DisplayNameProps } from './index'; import type { DisplayNameProps } from './index';
export const DisplayNameSimple: FC< export const DisplayNameSimple: FC<
@@ -12,12 +13,19 @@ export const DisplayNameSimple: FC<
if (!account) { if (!account) {
return null; return null;
} }
const accountName = isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html');
return ( return (
<bdi> <bdi>
<EmojiHTML {...props} htmlString={accountName} shallow as='span' /> <EmojiHTML
{...props}
as='span'
htmlString={
isModernEmojiEnabled()
? account.get('display_name')
: account.get('display_name_html')
}
extraEmojis={account.get('emojis')}
/>
</bdi> </bdi>
); );
}; };

View File

@@ -0,0 +1,108 @@
import type { MouseEventHandler, PropsWithChildren } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';
import classNames from 'classnames';
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
import { autoPlayGif } from '@/mastodon/initial_state';
import { polymorphicForwardRef } from '@/types/polymorphic';
import type {
CustomEmojiMapArg,
ExtraCustomEmojiMap,
} from 'mastodon/features/emoji/types';
// Animation context
export const AnimateEmojiContext = createContext<boolean | null>(null);
// Polymorphic provider component
type AnimateEmojiProviderProps = Required<PropsWithChildren> & {
className?: string;
};
export const AnimateEmojiProvider = polymorphicForwardRef<
'div',
AnimateEmojiProviderProps
>(
(
{
children,
as: Wrapper = 'div',
className,
onMouseEnter,
onMouseLeave,
...props
},
ref,
) => {
const [animate, setAnimate] = useState(autoPlayGif ?? false);
const handleEnter: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
onMouseEnter?.(event);
if (!autoPlayGif) {
setAnimate(true);
}
},
[onMouseEnter],
);
const handleLeave: MouseEventHandler<HTMLDivElement> = useCallback(
(event) => {
onMouseLeave?.(event);
if (!autoPlayGif) {
setAnimate(false);
}
},
[onMouseLeave],
);
// If there's a parent context or GIFs autoplay, we don't need handlers.
const parentContext = useContext(AnimateEmojiContext);
if (parentContext !== null || autoPlayGif === true) {
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
ref={ref}
>
{children}
</Wrapper>
);
}
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
ref={ref}
>
<AnimateEmojiContext.Provider value={animate}>
{children}
</AnimateEmojiContext.Provider>
</Wrapper>
);
},
);
AnimateEmojiProvider.displayName = 'AnimateEmojiProvider';
// Handle custom emoji
export const CustomEmojiContext = createContext<ExtraCustomEmojiMap>({});
export const CustomEmojiProvider = ({
children,
emojis: rawEmojis,
}: PropsWithChildren<{ emojis?: CustomEmojiMapArg }>) => {
const emojis = useMemo(() => cleanExtraEmojis(rawEmojis) ?? {}, [rawEmojis]);
return (
<CustomEmojiContext.Provider value={emojis}>
{children}
</CustomEmojiContext.Provider>
);
};

View File

@@ -0,0 +1,61 @@
import { useMemo } from 'react';
import type { ComponentPropsWithoutRef, ElementType } from 'react';
import classNames from 'classnames';
import type { CustomEmojiMapArg } from '@/mastodon/features/emoji/types';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { htmlStringToComponents } from '@/mastodon/utils/html';
import { AnimateEmojiProvider, CustomEmojiProvider } from './context';
import { textToEmojis } from './index';
type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
ComponentPropsWithoutRef<Element>,
'dangerouslySetInnerHTML' | 'className'
> & {
htmlString: string;
extraEmojis?: CustomEmojiMapArg;
as?: Element;
className?: string;
};
export const ModernEmojiHTML = ({
extraEmojis,
htmlString,
as: asProp = 'div', // Rename for syntax highlighting
shallow,
className = '',
...props
}: EmojiHTMLProps<ElementType>) => {
const contents = useMemo(
() => htmlStringToComponents(htmlString, { onText: textToEmojis }),
[htmlString],
);
return (
<CustomEmojiProvider emojis={extraEmojis}>
<AnimateEmojiProvider {...props} as={asProp} className={className}>
{contents}
</AnimateEmojiProvider>
</CustomEmojiProvider>
);
};
export const LegacyEmojiHTML = <Element extends ElementType>(
props: EmojiHTMLProps<Element>,
) => {
const { as: asElement, htmlString, extraEmojis, className, ...rest } = props;
const Wrapper = asElement ?? 'div';
return (
<Wrapper
{...rest}
dangerouslySetInnerHTML={{ __html: htmlString }}
className={classNames(className, 'animate-parent')}
/>
);
};
export const EmojiHTML = isModernEmojiEnabled()
? ModernEmojiHTML
: LegacyEmojiHTML;

View File

@@ -0,0 +1,99 @@
import type { FC } from 'react';
import { useContext, useEffect, useState } from 'react';
import { EMOJI_TYPE_CUSTOM } from '@/mastodon/features/emoji/constants';
import { useEmojiAppState } from '@/mastodon/features/emoji/hooks';
import { unicodeHexToUrl } from '@/mastodon/features/emoji/normalize';
import {
isStateLoaded,
loadEmojiDataToState,
shouldRenderImage,
stringToEmojiState,
tokenizeText,
} from '@/mastodon/features/emoji/render';
import { AnimateEmojiContext, CustomEmojiContext } from './context';
interface EmojiProps {
code: string;
showFallback?: boolean;
showLoading?: boolean;
}
export const Emoji: FC<EmojiProps> = ({
code,
showFallback = true,
showLoading = true,
}) => {
const customEmoji = useContext(CustomEmojiContext);
// First, set the emoji state based on the input code.
const [state, setState] = useState(() =>
stringToEmojiState(code, customEmoji),
);
// If we don't have data, then load emoji data asynchronously.
const appState = useEmojiAppState();
useEffect(() => {
if (state !== null) {
void loadEmojiDataToState(state, appState.currentLocale).then(setState);
}
}, [appState.currentLocale, state]);
const animate = useContext(AnimateEmojiContext);
const fallback = showFallback ? code : null;
// If the code is invalid or we otherwise know it's not valid, show the fallback.
if (!state) {
return fallback;
}
if (!shouldRenderImage(state, appState.mode)) {
return code;
}
if (!isStateLoaded(state)) {
if (showLoading) {
return <span className='emojione emoji-loading' title={code} />;
}
return fallback;
}
if (state.type === EMOJI_TYPE_CUSTOM) {
const shortcode = `:${state.code}:`;
return (
<img
src={animate ? state.data.url : state.data.static_url}
alt={shortcode}
title={shortcode}
className='emojione custom-emoji'
loading='lazy'
/>
);
}
const src = unicodeHexToUrl(state.code, appState.darkTheme);
return (
<img
src={src}
alt={state.data.unicode}
title={state.data.label}
className='emojione'
loading='lazy'
/>
);
};
/**
* Takes a text string and converts it to an array of React nodes.
* @param text The text to be tokenized and converted.
*/
export function textToEmojis(text: string) {
return tokenizeText(text).map((token, index) => {
if (typeof token === 'string') {
return token;
}
return <Emoji code={token.code} key={`emoji-${token.code}-${index}`} />;
});
}

View File

@@ -8,7 +8,6 @@ import { useIdentity } from '@/mastodon/identity_context';
import { import {
fetchRelationships, fetchRelationships,
followAccount, followAccount,
unblockAccount,
unmuteAccount, unmuteAccount,
} from 'mastodon/actions/accounts'; } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
@@ -59,7 +58,8 @@ export const FollowButton: React.FC<{
accountId?: string; accountId?: string;
compact?: boolean; compact?: boolean;
labelLength?: 'auto' | 'short' | 'long'; labelLength?: 'auto' | 'short' | 'long';
}> = ({ accountId, compact, labelLength = 'auto' }) => { className?: string;
}> = ({ accountId, compact, labelLength = 'auto', className }) => {
const intl = useIntl(); const intl = useIntl();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { signedIn } = useIdentity(); const { signedIn } = useIdentity();
@@ -96,12 +96,24 @@ export const FollowButton: React.FC<{
return; return;
} else if (relationship.muting) { } else if (relationship.muting) {
dispatch(unmuteAccount(accountId)); dispatch(unmuteAccount(accountId));
} else if (account && (relationship.following || relationship.requested)) { } else if (account && relationship.following) {
dispatch( dispatch(
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }), openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
); );
} else if (account && relationship.requested) {
dispatch(
openModal({
modalType: 'CONFIRM_WITHDRAW_REQUEST',
modalProps: { account },
}),
);
} else if (relationship.blocking) { } else if (relationship.blocking) {
dispatch(unblockAccount(accountId)); dispatch(
openModal({
modalType: 'CONFIRM_UNBLOCK',
modalProps: { account },
}),
);
} else { } else {
dispatch(followAccount(accountId)); dispatch(followAccount(accountId));
} }
@@ -144,7 +156,7 @@ export const FollowButton: React.FC<{
href='/settings/profile' href='/settings/profile'
target='_blank' target='_blank'
rel='noopener' rel='noopener'
className={classNames('button button-secondary', { className={classNames(className, 'button button-secondary', {
'button--compact': compact, 'button--compact': compact,
})} })}
> >
@@ -158,13 +170,12 @@ export const FollowButton: React.FC<{
onClick={handleClick} onClick={handleClick}
disabled={ disabled={
relationship?.blocked_by || relationship?.blocked_by ||
relationship?.blocking ||
(!(relationship?.following || relationship?.requested) && (!(relationship?.following || relationship?.requested) &&
(account?.suspended || !!account?.moved)) (account?.suspended || !!account?.moved))
} }
secondary={following} secondary={following}
compact={compact} compact={compact}
className={following ? 'button--destructive' : undefined} className={classNames(className, { 'button--destructive': following })}
> >
{label} {label}
</Button> </Button>

View File

@@ -13,10 +13,12 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { Poll } from 'mastodon/components/poll'; import { Poll } from 'mastodon/components/poll';
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; import { languages as preloadedLanguages } from 'mastodon/initial_state';
import { EmojiHTML } from '../features/emoji/emoji_html';
import { isModernEmojiEnabled } from '../utils/environment'; import { isModernEmojiEnabled } from '../utils/environment';
import { EmojiHTML } from './emoji/html';
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
/** /**

View File

@@ -5,7 +5,7 @@ import { fetchServer } from 'mastodon/actions/server';
import { hydrateStore } from 'mastodon/actions/store'; import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router'; import { Router } from 'mastodon/components/router';
import Compose from 'mastodon/features/standalone/compose'; import Compose from 'mastodon/features/standalone/compose';
import initialState from 'mastodon/initial_state'; import { initialState } from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { store } from 'mastodon/store'; import { store } from 'mastodon/store';

View File

@@ -13,7 +13,7 @@ import ErrorBoundary from 'mastodon/components/error_boundary';
import { Router } from 'mastodon/components/router'; import { Router } from 'mastodon/components/router';
import UI from 'mastodon/features/ui'; import UI from 'mastodon/features/ui';
import { IdentityContext, createIdentityContext } from 'mastodon/identity_context'; import { IdentityContext, createIdentityContext } from 'mastodon/identity_context';
import initialState, { title as siteTitle } from 'mastodon/initial_state'; import { initialState, title as siteTitle } from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { store } from 'mastodon/store'; import { store } from 'mastodon/store';
import { isProduction } from 'mastodon/utils/environment'; import { isProduction } from 'mastodon/utils/environment';

View File

@@ -8,6 +8,7 @@ import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/components/account_bio'; import { AccountBio } from '@/mastodon/components/account_bio';
import { DisplayName } from '@/mastodon/components/display_name'; import { DisplayName } from '@/mastodon/components/display_name';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@@ -33,7 +34,6 @@ import { initMuteModal } from 'mastodon/actions/mutes';
import { initReport } from 'mastodon/actions/reports'; import { initReport } from 'mastodon/actions/reports';
import { Avatar } from 'mastodon/components/avatar'; import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { CopyIconButton } from 'mastodon/components/copy_icon_button'; import { CopyIconButton } from 'mastodon/components/copy_icon_button';
import { import {
FollowersCounter, FollowersCounter,
@@ -383,7 +383,7 @@ export const AccountHeader: React.FC<{
const isRemote = account?.acct !== account?.username; const isRemote = account?.acct !== account?.username;
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
const menu = useMemo(() => { const menuItems = useMemo(() => {
const arr: MenuItem[] = []; const arr: MenuItem[] = [];
if (!account) { if (!account) {
@@ -605,6 +605,15 @@ export const AccountHeader: React.FC<{
handleUnblockDomain, handleUnblockDomain,
]); ]);
const menu = accountId !== me && (
<Dropdown
disabled={menuItems.length === 0}
items={menuItems}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
);
if (!account) { if (!account) {
return null; return null;
} }
@@ -718,21 +727,16 @@ export const AccountHeader: React.FC<{
); );
} }
if (relationship?.blocking) { const isMovedAndUnfollowedAccount = account.moved && !relationship?.following;
if (!isMovedAndUnfollowedAccount) {
actionBtn = ( actionBtn = (
<Button <FollowButton
text={intl.formatMessage(messages.unblock, { accountId={accountId}
name: account.username, className='account__header__follow-button'
})} labelLength='long'
onClick={handleBlock}
/> />
); );
} else {
actionBtn = <FollowButton accountId={accountId} />;
}
if (account.moved && !relationship?.following) {
actionBtn = '';
} }
if (account.locked) { if (account.locked) {
@@ -777,8 +781,8 @@ export const AccountHeader: React.FC<{
<MovedNote accountId={account.id} targetAccountId={account.moved} /> <MovedNote accountId={account.id} targetAccountId={account.moved} />
)} )}
<div <AnimateEmojiProvider
className={classNames('account__header animate-parent', { className={classNames('account__header', {
inactive: !!account.moved, inactive: !!account.moved,
})} })}
> >
@@ -814,18 +818,11 @@ export const AccountHeader: React.FC<{
/> />
</a> </a>
<div className='account__header__tabs__buttons'> <div className='account__header__buttons account__header__buttons--desktop'>
{!hidden && actionBtn}
{!hidden && bellBtn} {!hidden && bellBtn}
{!hidden && shareBtn} {!hidden && shareBtn}
{accountId !== me && ( {menu}
<Dropdown
disabled={menu.length === 0}
items={menu}
icon='ellipsis-v'
iconComponent={MoreHorizIcon}
/>
)}
{!hidden && actionBtn}
</div> </div>
</div> </div>
@@ -855,6 +852,12 @@ export const AccountHeader: React.FC<{
<FamiliarFollowers accountId={accountId} /> <FamiliarFollowers accountId={accountId} />
)} )}
<div className='account__header__buttons account__header__buttons--mobile'>
{!hidden && actionBtn}
{!hidden && bellBtn}
{menu}
</div>
{!(suspended || hidden) && ( {!(suspended || hidden) && (
<div className='account__header__extra'> <div className='account__header__extra'>
<div <div
@@ -967,7 +970,7 @@ export const AccountHeader: React.FC<{
</div> </div>
)} )}
</div> </div>
</div> </AnimateEmojiProvider>
{!(hideTabs || hidden) && ( {!(hideTabs || hidden) && (
<div className='account__section-headline'> <div className='account__section-headline'>

View File

@@ -25,6 +25,7 @@ import StatusContent from 'mastodon/components/status_content';
import { Dropdown } from 'mastodon/components/dropdown_menu'; import { Dropdown } from 'mastodon/components/dropdown_menu';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
import { LinkedDisplayName } from '@/mastodon/components/display_name'; import { LinkedDisplayName } from '@/mastodon/components/display_name';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
const messages = defineMessages({ const messages = defineMessages({
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
@@ -136,9 +137,9 @@ export const Conversation = ({ conversation, scrollKey }) => {
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} /> {unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
</div> </div>
<div className='conversation__content__names animate-parent'> <AnimateEmojiProvider className='conversation__content__names'>
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} /> <FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
</div> </AnimateEmojiProvider>
</div> </div>
<StatusContent <StatusContent

View File

@@ -23,8 +23,6 @@ export const EMOJI_MODE_TWEMOJI = 'twemoji';
export const EMOJI_TYPE_UNICODE = 'unicode'; export const EMOJI_TYPE_UNICODE = 'unicode';
export const EMOJI_TYPE_CUSTOM = 'custom'; export const EMOJI_TYPE_CUSTOM = 'custom';
export const EMOJI_STATE_MISSING = 'missing';
export const EMOJIS_WITH_DARK_BORDER = [ export const EMOJIS_WITH_DARK_BORDER = [
'🎱', // 1F3B1 '🎱', // 1F3B1
'🐜', // 1F41C '🐜', // 1F41C

View File

@@ -197,11 +197,18 @@ function toLoadedLocale(localeString: string) {
log(`Locale ${locale} is different from provided ${localeString}`); log(`Locale ${locale} is different from provided ${localeString}`);
} }
if (!loadedLocales.has(locale)) { if (!loadedLocales.has(locale)) {
throw new Error(`Locale ${locale} is not loaded in emoji database`); throw new LocaleNotLoadedError(locale);
} }
return locale; return locale;
} }
export class LocaleNotLoadedError extends Error {
constructor(locale: Locale) {
super(`Locale ${locale} is not loaded in emoji database`);
this.name = 'LocaleNotLoadedError';
}
}
async function hasLocale(locale: Locale, db: Database): Promise<boolean> { async function hasLocale(locale: Locale, db: Database): Promise<boolean> {
if (loadedLocales.has(locale)) { if (loadedLocales.has(locale)) {
return true; return true;

View File

@@ -1,70 +0,0 @@
import type { ComponentPropsWithoutRef, ElementType } from 'react';
import classNames from 'classnames';
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
import { useEmojify } from './hooks';
import type { CustomEmojiMapArg } from './types';
type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
ComponentPropsWithoutRef<Element>,
'dangerouslySetInnerHTML' | 'className'
> & {
htmlString: string;
extraEmojis?: CustomEmojiMapArg;
as?: Element;
shallow?: boolean;
className?: string;
};
export const ModernEmojiHTML = ({
extraEmojis,
htmlString,
as: Wrapper = 'div', // Rename for syntax highlighting
shallow,
className = '',
...props
}: EmojiHTMLProps<ElementType>) => {
const emojifiedHtml = useEmojify({
text: htmlString,
extraEmojis,
deep: !shallow,
});
if (emojifiedHtml === null) {
return null;
}
return (
<Wrapper
{...props}
className={classNames(className, 'animate-parent')}
dangerouslySetInnerHTML={{ __html: emojifiedHtml }}
/>
);
};
export const EmojiHTML = <Element extends ElementType>(
props: EmojiHTMLProps<Element>,
) => {
if (isModernEmojiEnabled()) {
return <ModernEmojiHTML {...props} />;
}
const {
as: asElement,
htmlString,
extraEmojis,
className,
shallow: _,
...rest
} = props;
const Wrapper = asElement ?? 'div';
return (
<Wrapper
{...rest}
dangerouslySetInnerHTML={{ __html: htmlString }}
className={classNames(className, 'animate-parent')}
/>
);
};

View File

@@ -1,4 +1,4 @@
import initialState from '@/mastodon/initial_state'; import { initialState } from '@/mastodon/initial_state';
import { loadWorker } from '@/mastodon/utils/workers'; import { loadWorker } from '@/mastodon/utils/workers';
import { toSupportedLocale } from './locale'; import { toSupportedLocale } from './locale';

View File

@@ -1,8 +1,6 @@
import { flattenEmojiData } from 'emojibase'; import { flattenEmojiData } from 'emojibase';
import type { CompactEmoji, FlatCompactEmoji } from 'emojibase'; import type { CompactEmoji, FlatCompactEmoji } from 'emojibase';
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
import { import {
putEmojiData, putEmojiData,
putCustomEmojiData, putCustomEmojiData,
@@ -10,7 +8,7 @@ import {
putLatestEtag, putLatestEtag,
} from './database'; } from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { LocaleOrCustom } from './types'; import type { CustomEmojiData, LocaleOrCustom } from './types';
import { emojiLogger } from './utils'; import { emojiLogger } from './utils';
const log = emojiLogger('loader'); const log = emojiLogger('loader');
@@ -27,7 +25,7 @@ export async function importEmojiData(localeString: string) {
} }
export async function importCustomEmojiData() { export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<ApiCustomEmojiJSON[]>('custom'); const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
if (!emojis) { if (!emojis) {
return; return;
} }

View File

@@ -5,11 +5,8 @@ import { flattenEmojiData } from 'emojibase';
import unicodeRawEmojis from 'emojibase-data/en/data.json'; import unicodeRawEmojis from 'emojibase-data/en/data.json';
import { import {
twemojiHasBorder,
twemojiToUnicodeInfo, twemojiToUnicodeInfo,
unicodeToTwemojiHex, unicodeToTwemojiHex,
CODES_WITH_DARK_BORDER,
CODES_WITH_LIGHT_BORDER,
emojiToUnicodeHex, emojiToUnicodeHex,
} from './normalize'; } from './normalize';
@@ -57,26 +54,6 @@ describe('unicodeToTwemojiHex', () => {
}); });
}); });
describe('twemojiHasBorder', () => {
test.concurrent.for(
svgFileNames
.filter((file) => file.endsWith('_border'))
.map((file) => {
const hexCode = file.replace('_border', '');
return [
hexCode,
CODES_WITH_LIGHT_BORDER.includes(hexCode.toUpperCase()),
CODES_WITH_DARK_BORDER.includes(hexCode.toUpperCase()),
] as const;
}),
)('twemojiHasBorder for %s', ([hexCode, isLight, isDark], { expect }) => {
const result = twemojiHasBorder(hexCode);
expect(result).toHaveProperty('hexCode', hexCode);
expect(result).toHaveProperty('hasLightBorder', isLight);
expect(result).toHaveProperty('hasDarkBorder', isDark);
});
});
describe('twemojiToUnicodeInfo', () => { describe('twemojiToUnicodeInfo', () => {
const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode)); const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode));

View File

@@ -1,5 +1,7 @@
import { isList } from 'immutable'; import { isList } from 'immutable';
import { assetHost } from '@/mastodon/utils/config';
import { import {
VARIATION_SELECTOR_CODE, VARIATION_SELECTOR_CODE,
KEYCAP_CODE, KEYCAP_CODE,
@@ -9,11 +11,7 @@ import {
EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_DARK_BORDER,
EMOJIS_WITH_LIGHT_BORDER, EMOJIS_WITH_LIGHT_BORDER,
} from './constants'; } from './constants';
import type { import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types';
CustomEmojiMapArg,
ExtraCustomEmojiMap,
TwemojiBorderInfo,
} from './types';
// Misc codes that have special handling // Misc codes that have special handling
const SKIER_CODE = 0x26f7; const SKIER_CODE = 0x26f7;
@@ -67,21 +65,17 @@ export const CODES_WITH_DARK_BORDER =
export const CODES_WITH_LIGHT_BORDER = export const CODES_WITH_LIGHT_BORDER =
EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex);
export function twemojiHasBorder(twemojiHex: string): TwemojiBorderInfo { export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string {
const normalizedHex = twemojiHex.toUpperCase(); const normalizedHex = unicodeToTwemojiHex(unicodeHex);
let hasLightBorder = false; let url = `${assetHost}/emoji/${normalizedHex}`;
let hasDarkBorder = false; if (darkMode && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) {
if (CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { url += '_border';
hasLightBorder = true;
} }
if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) { if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) {
hasDarkBorder = true; url += '_border';
} }
return { url += '.svg';
hexCode: twemojiHex, return url;
hasLightBorder,
hasDarkBorder,
};
} }
interface TwemojiSpecificEmoji { interface TwemojiSpecificEmoji {

View File

@@ -1,10 +1,6 @@
import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories'; import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories';
import { import { EMOJI_MODE_TWEMOJI } from './constants';
EMOJI_MODE_NATIVE,
EMOJI_MODE_NATIVE_WITH_FLAGS,
EMOJI_MODE_TWEMOJI,
} from './constants';
import * as db from './database'; import * as db from './database';
import { import {
emojifyElement, emojifyElement,
@@ -12,7 +8,7 @@ import {
testCacheClear, testCacheClear,
tokenizeText, tokenizeText,
} from './render'; } from './render';
import type { EmojiAppState, ExtraCustomEmojiMap } from './types'; import type { EmojiAppState } from './types';
function mockDatabase() { function mockDatabase() {
return { return {
@@ -40,18 +36,6 @@ const expectedSmileImage =
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">'; '<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
const expectedFlagImage = const expectedFlagImage =
'<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">'; '<img draggable="false" class="emojione" alt="🇪🇺" title="flag-eu" src="/emoji/1f1ea-1f1fa.svg">';
const expectedCustomEmojiImage =
'<img draggable="false" class="emojione custom-emoji" alt=":custom:" title=":custom:" src="emoji/custom/static" data-original="emoji/custom" data-static="emoji/custom/static">';
const expectedRemoteCustomEmojiImage =
'<img draggable="false" class="emojione custom-emoji" alt=":remote:" title=":remote:" src="remote.social/static" data-original="remote.social/custom" data-static="remote.social/static">';
const mockExtraCustom: ExtraCustomEmojiMap = {
remote: {
shortcode: 'remote',
static_url: 'remote.social/static',
url: 'remote.social/custom',
},
};
function testAppState(state: Partial<EmojiAppState> = {}) { function testAppState(state: Partial<EmojiAppState> = {}) {
return { return {
@@ -86,64 +70,10 @@ describe('emojifyElement', () => {
'en', 'en',
); );
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([ expect(searchCustomEmojisByShortcodes).toHaveBeenCalledExactlyOnceWith([
'custom', ':custom:',
]); ]);
}); });
test('emojifies custom emoji in native mode', async () => {
const { searchEmojisByHexcodes } = mockDatabase();
const actual = await emojifyElement(
testElement(),
testAppState({ mode: EMOJI_MODE_NATIVE }),
);
assert(actual);
expect(actual.innerHTML).toBe(
`<p>Hello 😊🇪🇺!</p><p>${expectedCustomEmojiImage}</p>`,
);
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
});
test('emojifies flag emoji in native-with-flags mode', async () => {
const { searchEmojisByHexcodes } = mockDatabase();
const actual = await emojifyElement(
testElement(),
testAppState({ mode: EMOJI_MODE_NATIVE_WITH_FLAGS }),
);
assert(actual);
expect(actual.innerHTML).toBe(
`<p>Hello 😊${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
);
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
});
test('emojifies everything in twemoji mode', async () => {
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
mockDatabase();
const actual = await emojifyElement(testElement(), testAppState());
assert(actual);
expect(actual.innerHTML).toBe(
`<p>Hello ${expectedSmileImage}${expectedFlagImage}!</p><p>${expectedCustomEmojiImage}</p>`,
);
expect(searchEmojisByHexcodes).toHaveBeenCalledOnce();
expect(searchCustomEmojisByShortcodes).toHaveBeenCalledOnce();
});
test('emojifies with provided custom emoji', async () => {
const { searchCustomEmojisByShortcodes, searchEmojisByHexcodes } =
mockDatabase();
const actual = await emojifyElement(
testElement('<p>hi :remote:</p>'),
testAppState(),
mockExtraCustom,
);
assert(actual);
expect(actual.innerHTML).toBe(
`<p>hi ${expectedRemoteCustomEmojiImage}</p>`,
);
expect(searchEmojisByHexcodes).not.toHaveBeenCalled();
expect(searchCustomEmojisByShortcodes).not.toHaveBeenCalled();
});
test('returns null when no emoji are found', async () => { test('returns null when no emoji are found', async () => {
mockDatabase(); mockDatabase();
const actual = await emojifyElement( const actual = await emojifyElement(
@@ -165,28 +95,9 @@ describe('emojifyText', () => {
const actual = await emojifyText('Hello 😊🇪🇺!', testAppState()); const actual = await emojifyText('Hello 😊🇪🇺!', testAppState());
expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`); expect(actual).toBe(`Hello ${expectedSmileImage}${expectedFlagImage}!`);
}); });
test('renders custom emojis', async () => {
mockDatabase();
const actual = await emojifyText('Hello :custom:!', testAppState());
expect(actual).toBe(`Hello ${expectedCustomEmojiImage}!`);
});
test('renders provided extra emojis', async () => {
const actual = await emojifyText(
'remote emoji :remote:',
testAppState(),
mockExtraCustom,
);
expect(actual).toBe(`remote emoji ${expectedRemoteCustomEmojiImage}`);
});
}); });
describe('tokenizeText', () => { describe('tokenizeText', () => {
test('returns empty array for string with only whitespace', () => {
expect(tokenizeText(' \n')).toEqual([]);
});
test('returns an array of text to be a single token', () => { test('returns an array of text to be a single token', () => {
expect(tokenizeText('Hello')).toEqual(['Hello']); expect(tokenizeText('Hello')).toEqual(['Hello']);
}); });
@@ -212,7 +123,7 @@ describe('tokenizeText', () => {
'Hello ', 'Hello ',
{ {
type: 'custom', type: 'custom',
code: 'smile', code: ':smile:',
}, },
'!!', '!!',
]); ]);
@@ -223,7 +134,7 @@ describe('tokenizeText', () => {
'Hello ', 'Hello ',
{ {
type: 'custom', type: 'custom',
code: 'smile_123', code: ':smile_123:',
}, },
'!!', '!!',
]); ]);
@@ -239,7 +150,7 @@ describe('tokenizeText', () => {
' ', ' ',
{ {
type: 'custom', type: 'custom',
code: 'smile', code: ':smile:',
}, },
'!!', '!!',
]); ]);

View File

@@ -1,6 +1,5 @@
import { autoPlayGif } from '@/mastodon/initial_state'; import { autoPlayGif } from '@/mastodon/initial_state';
import { createLimitedCache } from '@/mastodon/utils/cache'; import { createLimitedCache } from '@/mastodon/utils/cache';
import { assetHost } from '@/mastodon/utils/config';
import * as perf from '@/mastodon/utils/performance'; import * as perf from '@/mastodon/utils/performance';
import { import {
@@ -8,38 +7,130 @@ import {
EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_NATIVE_WITH_FLAGS,
EMOJI_TYPE_UNICODE, EMOJI_TYPE_UNICODE,
EMOJI_TYPE_CUSTOM, EMOJI_TYPE_CUSTOM,
EMOJI_STATE_MISSING,
} from './constants'; } from './constants';
import { import {
loadCustomEmojiByShortcode,
loadEmojiByHexcode,
LocaleNotLoadedError,
searchCustomEmojisByShortcodes, searchCustomEmojisByShortcodes,
searchEmojisByHexcodes, searchEmojisByHexcodes,
} from './database'; } from './database';
import { import { importEmojiData } from './loader';
emojiToUnicodeHex, import { emojiToUnicodeHex, unicodeHexToUrl } from './normalize';
twemojiHasBorder,
unicodeToTwemojiHex,
} from './normalize';
import type { import type {
CustomEmojiToken,
EmojiAppState, EmojiAppState,
EmojiLoadedState, EmojiLoadedState,
EmojiMode, EmojiMode,
EmojiState, EmojiState,
EmojiStateCustom,
EmojiStateMap, EmojiStateMap,
EmojiToken, EmojiStateUnicode,
ExtraCustomEmojiMap, ExtraCustomEmojiMap,
LocaleOrCustom, LocaleOrCustom,
UnicodeEmojiToken,
} from './types'; } from './types';
import { import {
anyEmojiRegex, anyEmojiRegex,
emojiLogger, emojiLogger,
isCustomEmoji,
isUnicodeEmoji,
stringHasAnyEmoji, stringHasAnyEmoji,
stringHasUnicodeFlags, stringHasUnicodeFlags,
} from './utils'; } from './utils';
const log = emojiLogger('render'); const log = emojiLogger('render');
/**
* Parses emoji string to extract emoji state.
* @param code Hex code or custom shortcode.
* @param customEmoji Extra custom emojis.
*/
export function stringToEmojiState(
code: string,
customEmoji: ExtraCustomEmojiMap = {},
): EmojiState | null {
if (isUnicodeEmoji(code)) {
return {
type: EMOJI_TYPE_UNICODE,
code: emojiToUnicodeHex(code),
};
}
if (isCustomEmoji(code)) {
const shortCode = code.slice(1, -1);
return {
type: EMOJI_TYPE_CUSTOM,
code: shortCode,
data: customEmoji[shortCode],
};
}
return null;
}
/**
* Loads emoji data into the given state if not already loaded.
* @param state Emoji state to load data for.
* @param locale Locale to load data for. Only for Unicode emoji.
* @param retry Internal. Whether this is a retry after loading the locale.
*/
export async function loadEmojiDataToState(
state: EmojiState,
locale: string,
retry = false,
): Promise<EmojiLoadedState | null> {
if (isStateLoaded(state)) {
return state;
}
// First, try to load the data from IndexedDB.
try {
// This is duplicative, but that's because TS can't distinguish the state type easily.
if (state.type === EMOJI_TYPE_UNICODE) {
const data = await loadEmojiByHexcode(state.code, locale);
if (data) {
return {
...state,
data,
};
}
} else {
const data = await loadCustomEmojiByShortcode(state.code);
if (data) {
return {
...state,
data,
};
}
}
// If not found, assume it's not an emoji and return null.
log(
'Could not find emoji %s of type %s for locale %s',
state.code,
state.type,
locale,
);
return null;
} catch (err: unknown) {
// If the locale is not loaded, load it and retry once.
if (!retry && err instanceof LocaleNotLoadedError) {
log(
'Error loading emoji %s for locale %s, loading locale and retrying.',
state.code,
locale,
);
await importEmojiData(locale); // Use this from the loader file as it can be awaited.
return loadEmojiDataToState(state, locale, true);
}
console.warn('Error loading emoji data, not retrying:', state, locale, err);
return null;
}
}
export function isStateLoaded(state: EmojiState): state is EmojiLoadedState {
return !!state.data;
}
/** /**
* Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions. * Emojifies an element. This modifies the element in place, replacing text nodes with emojified versions.
*/ */
@@ -177,7 +268,11 @@ async function textToElementArray(
if (token.type === EMOJI_TYPE_CUSTOM) { if (token.type === EMOJI_TYPE_CUSTOM) {
const extraEmojiData = extraEmojis[token.code]; const extraEmojiData = extraEmojis[token.code];
if (extraEmojiData) { if (extraEmojiData) {
state = { type: EMOJI_TYPE_CUSTOM, data: extraEmojiData }; state = {
type: EMOJI_TYPE_CUSTOM,
data: extraEmojiData,
code: token.code,
};
} else { } else {
state = emojiForLocale(token.code, EMOJI_TYPE_CUSTOM); state = emojiForLocale(token.code, EMOJI_TYPE_CUSTOM);
} }
@@ -189,7 +284,7 @@ async function textToElementArray(
} }
// If the state is valid, create an image element. Otherwise, just append as text. // If the state is valid, create an image element. Otherwise, just append as text.
if (state && typeof state !== 'string') { if (state && typeof state !== 'string' && isStateLoaded(state)) {
const image = stateToImage(state, appState); const image = stateToImage(state, appState);
renderedFragments.push(image); renderedFragments.push(image);
continue; continue;
@@ -202,11 +297,11 @@ async function textToElementArray(
return renderedFragments; return renderedFragments;
} }
type TokenizedText = (string | EmojiToken)[]; type TokenizedText = (string | EmojiState)[];
export function tokenizeText(text: string): TokenizedText { export function tokenizeText(text: string): TokenizedText {
if (!text.trim()) { if (!text.trim()) {
return []; return [text];
} }
const tokens = []; const tokens = [];
@@ -222,14 +317,14 @@ export function tokenizeText(text: string): TokenizedText {
// Custom emoji // Custom emoji
tokens.push({ tokens.push({
type: EMOJI_TYPE_CUSTOM, type: EMOJI_TYPE_CUSTOM,
code: code.slice(1, -1), // Remove the colons code,
} satisfies CustomEmojiToken); } satisfies EmojiStateCustom);
} else { } else {
// Unicode emoji // Unicode emoji
tokens.push({ tokens.push({
type: EMOJI_TYPE_UNICODE, type: EMOJI_TYPE_UNICODE,
code: code, code: code,
} satisfies UnicodeEmojiToken); } satisfies EmojiStateUnicode);
} }
lastIndex = match.index + code.length; lastIndex = match.index + code.length;
} }
@@ -304,13 +399,11 @@ async function loadMissingEmojiIntoCache(
const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale); const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale);
const cache = cacheForLocale(currentLocale); const cache = cacheForLocale(currentLocale);
for (const emoji of emojis) { for (const emoji of emojis) {
cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji }); cache.set(emoji.hexcode, {
} type: EMOJI_TYPE_UNICODE,
const notFoundEmojis = missingEmojis.filter((code) => data: emoji,
emojis.every((emoji) => emoji.hexcode !== code), code: emoji.hexcode,
); });
for (const code of notFoundEmojis) {
cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji.
} }
localeCacheMap.set(currentLocale, cache); localeCacheMap.set(currentLocale, cache);
} }
@@ -320,19 +413,17 @@ async function loadMissingEmojiIntoCache(
const emojis = await searchCustomEmojisByShortcodes(missingEmojis); const emojis = await searchCustomEmojisByShortcodes(missingEmojis);
const cache = cacheForLocale(EMOJI_TYPE_CUSTOM); const cache = cacheForLocale(EMOJI_TYPE_CUSTOM);
for (const emoji of emojis) { for (const emoji of emojis) {
cache.set(emoji.shortcode, { type: EMOJI_TYPE_CUSTOM, data: emoji }); cache.set(emoji.shortcode, {
} type: EMOJI_TYPE_CUSTOM,
const notFoundEmojis = missingEmojis.filter((code) => data: emoji,
emojis.every((emoji) => emoji.shortcode !== code), code: emoji.shortcode,
); });
for (const code of notFoundEmojis) {
cache.set(code, EMOJI_STATE_MISSING); // Mark as missing if not found, as it's probably not a valid emoji.
} }
localeCacheMap.set(EMOJI_TYPE_CUSTOM, cache); localeCacheMap.set(EMOJI_TYPE_CUSTOM, cache);
} }
} }
function shouldRenderImage(token: EmojiToken, mode: EmojiMode): boolean { export function shouldRenderImage(token: EmojiState, mode: EmojiMode): boolean {
if (token.type === EMOJI_TYPE_UNICODE) { if (token.type === EMOJI_TYPE_UNICODE) {
// If the mode is native or native with flags for non-flag emoji // If the mode is native or native with flags for non-flag emoji
// we can just append the text node directly. // we can just append the text node directly.
@@ -354,18 +445,9 @@ function stateToImage(state: EmojiLoadedState, appState: EmojiAppState) {
image.classList.add('emojione'); image.classList.add('emojione');
if (state.type === EMOJI_TYPE_UNICODE) { if (state.type === EMOJI_TYPE_UNICODE) {
const emojiInfo = twemojiHasBorder(unicodeToTwemojiHex(state.data.hexcode));
let fileName = emojiInfo.hexCode;
if (
(appState.darkTheme && emojiInfo.hasDarkBorder) ||
(!appState.darkTheme && emojiInfo.hasLightBorder)
) {
fileName = `${emojiInfo.hexCode}_border`;
}
image.alt = state.data.unicode; image.alt = state.data.unicode;
image.title = state.data.label; image.title = state.data.label;
image.src = `${assetHost}/emoji/${fileName}.svg`; image.src = unicodeHexToUrl(state.data.hexcode, appState.darkTheme);
} else { } else {
// Custom emoji // Custom emoji
const shortCode = `:${state.data.shortcode}:`; const shortCode = `:${state.data.shortcode}:`;

View File

@@ -10,7 +10,6 @@ import type {
EMOJI_MODE_NATIVE, EMOJI_MODE_NATIVE,
EMOJI_MODE_NATIVE_WITH_FLAGS, EMOJI_MODE_NATIVE_WITH_FLAGS,
EMOJI_MODE_TWEMOJI, EMOJI_MODE_TWEMOJI,
EMOJI_STATE_MISSING,
EMOJI_TYPE_CUSTOM, EMOJI_TYPE_CUSTOM,
EMOJI_TYPE_UNICODE, EMOJI_TYPE_UNICODE,
} from './constants'; } from './constants';
@@ -29,45 +28,40 @@ export interface EmojiAppState {
darkTheme: boolean; darkTheme: boolean;
} }
export interface UnicodeEmojiToken {
type: typeof EMOJI_TYPE_UNICODE;
code: string;
}
export interface CustomEmojiToken {
type: typeof EMOJI_TYPE_CUSTOM;
code: string;
}
export type EmojiToken = UnicodeEmojiToken | CustomEmojiToken;
export type CustomEmojiData = ApiCustomEmojiJSON; export type CustomEmojiData = ApiCustomEmojiJSON;
export type UnicodeEmojiData = FlatCompactEmoji; export type UnicodeEmojiData = FlatCompactEmoji;
export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData; export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData;
export type EmojiStateMissing = typeof EMOJI_STATE_MISSING; type CustomEmojiRenderFields = Pick<
CustomEmojiData,
'shortcode' | 'static_url' | 'url'
>;
export interface EmojiStateUnicode { export interface EmojiStateUnicode {
type: typeof EMOJI_TYPE_UNICODE; type: typeof EMOJI_TYPE_UNICODE;
data: UnicodeEmojiData; code: string;
data?: UnicodeEmojiData;
} }
export interface EmojiStateCustom { export interface EmojiStateCustom {
type: typeof EMOJI_TYPE_CUSTOM; type: typeof EMOJI_TYPE_CUSTOM;
data: CustomEmojiRenderFields; code: string;
data?: CustomEmojiRenderFields;
} }
export type EmojiState = export type EmojiState = EmojiStateUnicode | EmojiStateCustom;
| EmojiStateMissing export type EmojiLoadedState =
| EmojiStateUnicode | Required<EmojiStateUnicode>
| EmojiStateCustom; | Required<EmojiStateCustom>;
export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom;
export type EmojiStateMap = LimitedCache<string, EmojiState>; export type EmojiStateMap = LimitedCache<string, EmojiState>;
export type CustomEmojiMapArg = export type CustomEmojiMapArg =
| ExtraCustomEmojiMap | ExtraCustomEmojiMap
| ImmutableList<CustomEmoji>; | ImmutableList<CustomEmoji>;
export type CustomEmojiRenderFields = Pick<
CustomEmojiData, export type ExtraCustomEmojiMap = Record<
'shortcode' | 'static_url' | 'url' string,
Pick<CustomEmojiData, 'shortcode' | 'static_url' | 'url'>
>; >;
export type ExtraCustomEmojiMap = Record<string, CustomEmojiRenderFields>;
export interface TwemojiBorderInfo { export interface TwemojiBorderInfo {
hexCode: string; hexCode: string;

View File

@@ -10,6 +10,13 @@ export function stringHasUnicodeEmoji(input: string): boolean {
return new RegExp(EMOJI_REGEX, supportedFlags()).test(input); return new RegExp(EMOJI_REGEX, supportedFlags()).test(input);
} }
export function isUnicodeEmoji(input: string): boolean {
return (
input.length > 0 &&
new RegExp(`^(${EMOJI_REGEX})+$`, supportedFlags()).test(input)
);
}
export function stringHasUnicodeFlags(input: string): boolean { export function stringHasUnicodeFlags(input: string): boolean {
if (supportsRegExpSets()) { if (supportsRegExpSets()) {
return new RegExp( return new RegExp(
@@ -27,6 +34,11 @@ export function stringHasUnicodeFlags(input: string): boolean {
// Constant as this is supported by all browsers. // Constant as this is supported by all browsers.
const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i; const CUSTOM_EMOJI_REGEX = /:([a-z0-9_]+):/i;
export function isCustomEmoji(input: string): boolean {
return new RegExp(`^${CUSTOM_EMOJI_REGEX.source}$`, 'i').test(input);
}
export function stringHasCustomEmoji(input: string) { export function stringHasCustomEmoji(input: string) {
return CUSTOM_EMOJI_REGEX.test(input); return CUSTOM_EMOJI_REGEX.test(input);
} }

View File

@@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
import type { List as ImmutableList, RecordOf } from 'immutable'; import type { List as ImmutableList, RecordOf } from 'immutable';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
import { toggleStatusSpoilers } from 'mastodon/actions/statuses'; import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
@@ -96,8 +97,8 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
).size; ).size;
return ( return (
<div <AnimateEmojiProvider
className='notification-group__embedded-status animate-parent' className='notification-group__embedded-status'
role='button' role='button'
tabIndex={-1} tabIndex={-1}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
@@ -148,6 +149,6 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
)} )}
</div> </div>
)} )}
</div> </AnimateEmojiProvider>
); );
}; };

View File

@@ -2,25 +2,19 @@ import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react';
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
import StarIcon from '@/material-icons/400-24px/star-fill.svg?react'; import StarIcon from '@/material-icons/400-24px/star-fill.svg?react';
import StarBorderIcon from '@/material-icons/400-24px/star.svg?react'; import StarBorderIcon from '@/material-icons/400-24px/star.svg?react';
import RepeatActiveIcon from '@/svg-icons/repeat_active.svg?react';
import RepeatDisabledIcon from '@/svg-icons/repeat_disabled.svg?react';
import RepeatPrivateIcon from '@/svg-icons/repeat_private.svg?react';
import RepeatPrivateActiveIcon from '@/svg-icons/repeat_private_active.svg?react';
import { replyCompose } from 'mastodon/actions/compose'; import { replyCompose } from 'mastodon/actions/compose';
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions'; import { toggleFavourite } from 'mastodon/actions/interactions';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import { BoostButton } from 'mastodon/components/status/boost_button';
import { useIdentity } from 'mastodon/identity_context'; import { useIdentity } from 'mastodon/identity_context';
import { me } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account'; import type { Account } from 'mastodon/models/account';
import type { Status } from 'mastodon/models/status'; import type { Status } from 'mastodon/models/status';
import { makeGetStatus } from 'mastodon/selectors'; import { makeGetStatus } from 'mastodon/selectors';
@@ -120,29 +114,6 @@ export const Footer: React.FC<{
} }
}, [dispatch, status, signedIn]); }, [dispatch, status, signedIn]);
const handleReblogClick = useCallback(
(e: React.MouseEvent) => {
if (!status) {
return;
}
if (signedIn) {
dispatch(toggleReblog(status.get('id'), e.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, signedIn],
);
const handleOpenClick = useCallback( const handleOpenClick = useCallback(
(e: React.MouseEvent) => { (e: React.MouseEvent) => {
if (e.button !== 0 || !status) { if (e.button !== 0 || !status) {
@@ -160,13 +131,6 @@ export const Footer: React.FC<{
return null; return null;
} }
const publicStatus = ['public', 'unlisted'].includes(
status.get('visibility') as string,
);
const reblogPrivate =
status.getIn(['account', 'id']) === me &&
status.get('visibility') === 'private';
let replyIcon, replyIconComponent, replyTitle; let replyIcon, replyIconComponent, replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
@@ -179,24 +143,6 @@ export const Footer: React.FC<{
replyTitle = intl.formatMessage(messages.replyAll); replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle, reblogIconComponent;
if (status.get('reblogged')) {
reblogTitle = intl.formatMessage(messages.cancel_reblog_private);
reblogIconComponent = publicStatus
? RepeatActiveIcon
: RepeatPrivateActiveIcon;
} else if (publicStatus) {
reblogTitle = intl.formatMessage(messages.reblog);
reblogIconComponent = RepeatIcon;
} else if (reblogPrivate) {
reblogTitle = intl.formatMessage(messages.reblog_private);
reblogIconComponent = RepeatPrivateIcon;
} else {
reblogTitle = intl.formatMessage(messages.cannot_reblog);
reblogIconComponent = RepeatDisabledIcon;
}
const favouriteTitle = intl.formatMessage( const favouriteTitle = intl.formatMessage(
status.get('favourited') ? messages.removeFavourite : messages.favourite, status.get('favourited') ? messages.removeFavourite : messages.favourite,
); );
@@ -222,19 +168,7 @@ export const Footer: React.FC<{
counter={status.get('replies_count') as number} counter={status.get('replies_count') as number}
/> />
<IconButton <BoostButton counters status={status} />
className={classNames('status__action-bar-button', { reblogPrivate })}
disabled={!publicStatus && !reblogPrivate}
active={status.get('reblogged') as boolean}
title={reblogTitle}
icon='retweet'
iconComponent={reblogIconComponent}
onClick={handleReblogClick}
counter={
(status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
}
/>
<IconButton <IconButton
className='status__action-bar-button star-icon' className='status__action-bar-button star-icon'

View File

@@ -12,6 +12,8 @@ import { ColumnHeader } from 'mastodon/components/column_header';
import { Icon } from 'mastodon/components/icon'; import { Icon } from 'mastodon/components/icon';
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import StatusList from 'mastodon/components/status_list'; import StatusList from 'mastodon/components/status_list';
import { useIdentity } from 'mastodon/identity_context';
import { domain } from 'mastodon/initial_state';
import { useAppDispatch, useAppSelector } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
@@ -31,9 +33,18 @@ export const Quotes: React.FC<{
const statusId = params?.statusId; const statusId = params?.statusId;
const { accountId: me } = useIdentity();
const isCorrectStatusId: boolean = useAppSelector( const isCorrectStatusId: boolean = useAppSelector(
(state) => state.status_lists.getIn(['quotes', 'statusId']) === statusId, (state) => state.status_lists.getIn(['quotes', 'statusId']) === statusId,
); );
const quotedAccountId = useAppSelector(
(state) =>
state.statuses.getIn([statusId, 'account']) as string | undefined,
);
const quotedAccount = useAppSelector((state) =>
quotedAccountId ? state.accounts.get(quotedAccountId) : undefined,
);
const statusIds = useAppSelector((state) => const statusIds = useAppSelector((state) =>
state.status_lists.getIn(['quotes', 'items'], emptyList), state.status_lists.getIn(['quotes', 'items'], emptyList),
); );
@@ -74,6 +85,32 @@ export const Quotes: React.FC<{
/> />
); );
let prependMessage;
if (me === quotedAccountId) {
prependMessage = null;
} else if (quotedAccount?.username === quotedAccount?.acct) {
// Local account, we know this to be exhaustive
prependMessage = (
<div className='follow_requests-unlocked_explanation'>
<FormattedMessage
id='status.quotes.local_other_disclaimer'
defaultMessage='Quotes rejected by the author will not be shown.'
/>
</div>
);
} else {
prependMessage = (
<div className='follow_requests-unlocked_explanation'>
<FormattedMessage
id='status.quotes.remote_other_disclaimer'
defaultMessage='Only quotes from {domain} are guaranteed to be shown here. Quotes rejected by the author will not be shown.'
values={{ domain: <strong>{domain}</strong> }}
/>
</div>
);
}
return ( return (
<Column bindToDocument={!multiColumn}> <Column bindToDocument={!multiColumn}>
<ColumnHeader <ColumnHeader
@@ -100,6 +137,7 @@ export const Quotes: React.FC<{
isLoading={isLoading} isLoading={isLoading}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
bindToDocument={!multiColumn} bindToDocument={!multiColumn}
prepend={prependMessage}
/> />
<Helmet> <Helmet>

View File

@@ -11,7 +11,7 @@ import { hydrateStore } from 'mastodon/actions/store';
import { Router } from 'mastodon/components/router'; import { Router } from 'mastodon/components/router';
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status'; import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal'; import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
import initialState from 'mastodon/initial_state'; import { initialState } from 'mastodon/initial_state';
import { IntlProvider } from 'mastodon/locales'; import { IntlProvider } from 'mastodon/locales';
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors'; import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
import { store, useAppSelector, useAppDispatch } from 'mastodon/store'; import { store, useAppSelector, useAppDispatch } from 'mastodon/store';

View File

@@ -31,7 +31,7 @@ import { VisibilityIcon } from 'mastodon/components/visibility_icon';
import { Audio } from 'mastodon/features/audio'; import { Audio } from 'mastodon/features/audio';
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task'; import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
import { Video } from 'mastodon/features/video'; import { Video } from 'mastodon/features/video';
import { me } from 'mastodon/initial_state'; import { useIdentity } from 'mastodon/identity_context';
import Card from './card'; import Card from './card';
@@ -75,6 +75,8 @@ export const DetailedStatus: React.FC<{
const [showDespiteFilter, setShowDespiteFilter] = useState(false); const [showDespiteFilter, setShowDespiteFilter] = useState(false);
const nodeRef = useRef<HTMLDivElement>(); const nodeRef = useRef<HTMLDivElement>();
const { signedIn } = useIdentity();
const handleOpenVideo = useCallback( const handleOpenVideo = useCallback(
(options: VideoModalOptions) => { (options: VideoModalOptions) => {
const lang = (status.getIn(['translation', 'language']) || const lang = (status.getIn(['translation', 'language']) ||
@@ -283,7 +285,7 @@ export const DetailedStatus: React.FC<{
if (['private', 'direct'].includes(status.get('visibility') as string)) { if (['private', 'direct'].includes(status.get('visibility') as string)) {
quotesLink = ''; quotesLink = '';
} else if (status.getIn(['account', 'id']) === me) { } else if (signedIn) {
quotesLink = ( quotesLink = (
<Link <Link
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/quotes`} to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/quotes`}

View File

@@ -11,7 +11,7 @@ export interface BaseConfirmationModalProps {
export const ConfirmationModal: React.FC< export const ConfirmationModal: React.FC<
{ {
title: React.ReactNode; title: React.ReactNode;
message: React.ReactNode; message?: React.ReactNode;
confirm: React.ReactNode; confirm: React.ReactNode;
cancel?: React.ReactNode; cancel?: React.ReactNode;
secondary?: React.ReactNode; secondary?: React.ReactNode;
@@ -48,7 +48,7 @@ export const ConfirmationModal: React.FC<
<div className='safety-action-modal__top'> <div className='safety-action-modal__top'>
<div className='safety-action-modal__confirmation'> <div className='safety-action-modal__confirmation'>
<h1>{title}</h1> <h1>{title}</h1>
<p>{message}</p> {message && <p>{message}</p>}
</div> </div>
</div> </div>

View File

@@ -5,7 +5,9 @@ export {
ConfirmReplyModal, ConfirmReplyModal,
ConfirmEditStatusModal, ConfirmEditStatusModal,
} from './discard_draft_confirmation'; } from './discard_draft_confirmation';
export { ConfirmWithdrawRequestModal } from './withdraw_follow_request';
export { ConfirmUnfollowModal } from './unfollow'; export { ConfirmUnfollowModal } from './unfollow';
export { ConfirmUnblockModal } from './unblock';
export { ConfirmClearNotificationsModal } from './clear_notifications'; export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out'; export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list'; export { ConfirmFollowToListModal } from './follow_to_list';

View File

@@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { unblockAccount } from 'mastodon/actions/accounts';
import type { Account } from 'mastodon/models/account';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
unblockConfirm: {
id: 'confirmations.unblock.confirm',
defaultMessage: 'Unblock',
},
});
export const ConfirmUnblockModal: React.FC<
{
account: Account;
} & BaseConfirmationModalProps
> = ({ account, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(unblockAccount(account.id));
}, [dispatch, account.id]);
return (
<ConfirmationModal
title={
<FormattedMessage
id='confirmations.unblock.title'
defaultMessage='Unblock {name}?'
values={{ name: `@${account.acct}` }}
/>
}
confirm={intl.formatMessage(messages.unblockConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -10,10 +10,6 @@ import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal'; import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({ const messages = defineMessages({
unfollowTitle: {
id: 'confirmations.unfollow.title',
defaultMessage: 'Unfollow user?',
},
unfollowConfirm: { unfollowConfirm: {
id: 'confirmations.unfollow.confirm', id: 'confirmations.unfollow.confirm',
defaultMessage: 'Unfollow', defaultMessage: 'Unfollow',
@@ -34,12 +30,11 @@ export const ConfirmUnfollowModal: React.FC<
return ( return (
<ConfirmationModal <ConfirmationModal
title={intl.formatMessage(messages.unfollowTitle)} title={
message={
<FormattedMessage <FormattedMessage
id='confirmations.unfollow.message' id='confirmations.unfollow.title'
defaultMessage='Are you sure you want to unfollow {name}?' defaultMessage='Unfollow {name}?'
values={{ name: <strong>@{account.acct}</strong> }} values={{ name: `@${account.acct}` }}
/> />
} }
confirm={intl.formatMessage(messages.unfollowConfirm)} confirm={intl.formatMessage(messages.unfollowConfirm)}

View File

@@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { unfollowAccount } from 'mastodon/actions/accounts';
import type { Account } from 'mastodon/models/account';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
withdrawConfirm: {
id: 'confirmations.withdraw_request.confirm',
defaultMessage: 'Withdraw request',
},
});
export const ConfirmWithdrawRequestModal: React.FC<
{
account: Account;
} & BaseConfirmationModalProps
> = ({ account, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
dispatch(unfollowAccount(account.id));
}, [dispatch, account.id]);
return (
<ConfirmationModal
title={
<FormattedMessage
id='confirmations.withdraw_request.title'
defaultMessage='Withdraw request to follow {name}?'
values={{ name: `@${account.acct}` }}
/>
}
confirm={intl.formatMessage(messages.withdrawConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@@ -32,7 +32,9 @@ import {
ConfirmDeleteListModal, ConfirmDeleteListModal,
ConfirmReplyModal, ConfirmReplyModal,
ConfirmEditStatusModal, ConfirmEditStatusModal,
ConfirmUnblockModal,
ConfirmUnfollowModal, ConfirmUnfollowModal,
ConfirmWithdrawRequestModal,
ConfirmClearNotificationsModal, ConfirmClearNotificationsModal,
ConfirmLogOutModal, ConfirmLogOutModal,
ConfirmFollowToListModal, ConfirmFollowToListModal,
@@ -57,7 +59,9 @@ export const MODAL_COMPONENTS = {
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }), 'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }), 'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }), 'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
'CONFIRM_UNBLOCK': () => Promise.resolve({ default: ConfirmUnblockModal }),
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }), 'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
'CONFIRM_WITHDRAW_REQUEST': () => Promise.resolve({ default: ConfirmWithdrawRequestModal }),
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }), 'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }), 'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }), 'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),

View File

@@ -27,7 +27,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act
import { clearHeight } from '../../actions/height_cache'; import { clearHeight } from '../../actions/height_cache';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines'; import { expandHomeTimeline } from '../../actions/timelines';
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state'; import { initialState, me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state';
import BundleColumnError from './components/bundle_column_error'; import BundleColumnError from './components/bundle_column_error';
import { NavigationBar } from './components/navigation_bar'; import { NavigationBar } from './components/navigation_bar';

View File

@@ -1,4 +1,4 @@
import initialState from '@/mastodon/initial_state'; import { initialState } from '@/mastodon/initial_state';
interface FocusColumnOptions { interface FocusColumnOptions {
index?: number; index?: number;

View File

@@ -1,145 +0,0 @@
// @ts-check
/**
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
*/
/**
* @typedef InitialStateMeta
* @property {string} access_token
* @property {boolean=} advanced_layout
* @property {boolean} auto_play_gif
* @property {boolean} activity_api_enabled
* @property {string} admin
* @property {boolean=} boost_modal
* @property {boolean=} delete_modal
* @property {boolean=} missing_alt_text_modal
* @property {boolean=} disable_swiping
* @property {boolean=} disable_hover_cards
* @property {string=} disabled_account_id
* @property {string} display_media
* @property {string} domain
* @property {boolean=} expand_spoilers
* @property {boolean} limited_federation_mode
* @property {string} locale
* @property {string | null} mascot
* @property {string=} me
* @property {string=} moved_to_account_id
* @property {string=} owner
* @property {boolean} profile_directory
* @property {boolean} registrations_open
* @property {boolean} reduce_motion
* @property {string} repository
* @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} single_user_mode
* @property {string} source_url
* @property {string} streaming_api_base_url
* @property {boolean} timeline_preview
* @property {string} title
* @property {boolean} show_trends
* @property {boolean} trends_as_landing_page
* @property {boolean} use_blurhash
* @property {boolean=} use_pending_items
* @property {string} version
* @property {string} sso_redirect
* @property {string} status_page_url
* @property {boolean} terms_of_service_enabled
* @property {string?} emoji_style
*/
/**
* @typedef Role
* @property {string} id
* @property {string} name
* @property {string} permissions
* @property {string} color
* @property {boolean} highlighted
*/
/**
* @typedef InitialState
* @property {Record<string, import("./api_types/accounts").ApiAccountJSON>} accounts
* @property {InitialStateLanguage[]} languages
* @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
* @property {Role?} role
* @property {string[]} features
*/
const element = document.getElementById('initial-state');
/** @type {InitialState | undefined} */
const initialState = element?.textContent && JSON.parse(element.textContent);
/** @type {string} */
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
/** @type {boolean} */
export const hasMultiColumnPath = initialPath === '/'
|| initialPath === '/getting-started'
|| initialPath === '/home'
|| initialPath.startsWith('/deck');
/**
* @template {keyof InitialStateMeta} K
* @param {K} prop
* @returns {InitialStateMeta[K] | undefined}
*/
const getMeta = (prop) => initialState?.meta && initialState.meta[prop];
export const activityApiEnabled = getMeta('activity_api_enabled');
export const autoPlayGif = getMeta('auto_play_gif');
export const boostModal = getMeta('boost_modal');
export const deleteModal = getMeta('delete_modal');
export const missingAltTextModal = getMeta('missing_alt_text_modal');
export const disableSwiping = getMeta('disable_swiping');
export const disableHoverCards = getMeta('disable_hover_cards');
export const disabledAccountId = getMeta('disabled_account_id');
export const displayMedia = getMeta('display_media');
export const domain = getMeta('domain');
export const emojiStyle = getMeta('emoji_style') || 'auto';
export const expandSpoilers = getMeta('expand_spoilers');
export const forceSingleColumn = !getMeta('advanced_layout');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot');
export const me = getMeta('me');
export const movedToAccountId = getMeta('moved_to_account_id');
export const owner = getMeta('owner');
export const profile_directory = getMeta('profile_directory');
export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview');
export const title = getMeta('title');
export const trendsAsLanding = getMeta('trends_as_landing_page');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const criticalUpdatesPending = initialState?.critical_updates_pending;
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
const displayNames = Intl.DisplayNames && new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
});
export const languages = initialState?.languages?.map(lang => {
// zh-YUE is not a valid CLDR unicode_language_id
return [lang[0], displayNames?.of(lang[0].replace('zh-YUE', 'yue')) || lang[1], lang[2]];
});
/**
* @returns {string | undefined}
*/
export function getAccessToken() {
return getMeta('access_token');
}
export default initialState;

View File

@@ -0,0 +1,141 @@
import type { ApiAccountJSON } from './api_types/accounts';
type InitialStateLanguage = [code: string, name: string, localName: string];
interface InitialStateMeta {
access_token: string;
advanced_layout?: boolean;
auto_play_gif: boolean;
activity_api_enabled: boolean;
admin: string;
boost_modal?: boolean;
delete_modal?: boolean;
missing_alt_text_modal?: boolean;
disable_swiping?: boolean;
disable_hover_cards?: boolean;
disabled_account_id?: string;
display_media: string;
domain: string;
expand_spoilers?: boolean;
limited_federation_mode: boolean;
locale: string;
mascot: string | null;
me?: string;
moved_to_account_id?: string;
owner?: string;
profile_directory: boolean;
registrations_open: boolean;
reduce_motion: boolean;
repository: string;
search_enabled: boolean;
trends_enabled: boolean;
single_user_mode: boolean;
source_url: string;
streaming_api_base_url: string;
timeline_preview: boolean;
title: string;
show_trends: boolean;
trends_as_landing_page: boolean;
use_blurhash: boolean;
use_pending_items?: boolean;
version: string;
sso_redirect: string;
status_page_url: string;
terms_of_service_enabled: boolean;
emoji_style?: string;
}
interface Role {
id: string;
name: string;
permissions: string;
color: string;
highlighted: boolean;
}
export interface InitialState {
accounts: Record<string, ApiAccountJSON>;
languages: InitialStateLanguage[];
critical_updates_pending?: boolean;
meta: InitialStateMeta;
role?: Role;
features: string[];
}
const element = document.getElementById('initial-state');
export const initialState: InitialState | undefined = element?.textContent
? (JSON.parse(element.textContent) as InitialState)
: undefined;
const initialPath: string =
document
.querySelector('head meta[name=initialPath]')
?.getAttribute('content') ?? '';
export const hasMultiColumnPath: boolean =
initialPath === '/' ||
initialPath === '/getting-started' ||
initialPath === '/home' ||
initialPath.startsWith('/deck');
function getMeta<K extends keyof InitialStateMeta>(
prop: K,
): InitialStateMeta[K] | undefined {
return initialState?.meta[prop];
}
export const activityApiEnabled = getMeta('activity_api_enabled');
export const autoPlayGif = getMeta('auto_play_gif');
export const boostModal = getMeta('boost_modal');
export const deleteModal = getMeta('delete_modal');
export const missingAltTextModal = getMeta('missing_alt_text_modal');
export const disableSwiping = getMeta('disable_swiping');
export const disableHoverCards = getMeta('disable_hover_cards');
export const disabledAccountId = getMeta('disabled_account_id');
export const displayMedia = getMeta('display_media');
export const domain = getMeta('domain');
export const emojiStyle = getMeta('emoji_style') ?? 'auto';
export const expandSpoilers = getMeta('expand_spoilers');
export const forceSingleColumn = !getMeta('advanced_layout');
export const limitedFederationMode = getMeta('limited_federation_mode');
export const mascot = getMeta('mascot');
export const me = getMeta('me');
export const movedToAccountId = getMeta('moved_to_account_id');
export const owner = getMeta('owner');
export const profile_directory = getMeta('profile_directory');
export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
export const trendsEnabled = getMeta('trends_enabled');
export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview');
export const title = getMeta('title');
export const trendsAsLanding = getMeta('trends_as_landing_page');
export const useBlurhash = getMeta('use_blurhash');
export const usePendingItems = getMeta('use_pending_items');
export const version = getMeta('version');
export const criticalUpdatesPending = initialState?.critical_updates_pending;
export const statusPageUrl = getMeta('status_page_url');
export const sso_redirect = getMeta('sso_redirect');
export const termsOfServiceEnabled = getMeta('terms_of_service_enabled');
const displayNames = new Intl.DisplayNames(getMeta('locale'), {
type: 'language',
fallback: 'none',
languageDisplay: 'standard',
});
export const languages = initialState?.languages.map((lang) => {
// zh-YUE is not a valid CLDR unicode_language_id
return [
lang[0],
displayNames.of(lang[0].replace('zh-YUE', 'yue')) ?? lang[1],
lang[2],
];
});
export function getAccessToken(): string | undefined {
return getMeta('access_token');
}

View File

@@ -133,7 +133,6 @@
"confirmations.mute.confirm": "Silenciar", "confirmations.mute.confirm": "Silenciar",
"confirmations.redraft.confirm": "Borrar y tornar ta borrador", "confirmations.redraft.confirm": "Borrar y tornar ta borrador",
"confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Yes seguro que quiers deixar de seguir a {name}?",
"conversation.delete": "Borrar conversación", "conversation.delete": "Borrar conversación",
"conversation.mark_as_read": "Marcar como leyiu", "conversation.mark_as_read": "Marcar como leyiu",
"conversation.open": "Veyer conversación", "conversation.open": "Veyer conversación",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "لا يمكن التراجع عن هذا الإجراء.", "confirmations.revoke_quote.message": "لا يمكن التراجع عن هذا الإجراء.",
"confirmations.revoke_quote.title": "أتريد إزالة المنشور؟", "confirmations.revoke_quote.title": "أتريد إزالة المنشور؟",
"confirmations.unfollow.confirm": "إلغاء المتابعة", "confirmations.unfollow.confirm": "إلغاء المتابعة",
"confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟",
"confirmations.unfollow.title": "إلغاء متابعة المستخدم؟",
"content_warning.hide": "إخفاء المنشور", "content_warning.hide": "إخفاء المنشور",
"content_warning.show": "إظهار على أي حال", "content_warning.show": "إظهار على أي حال",
"content_warning.show_more": "إظهار المزيد", "content_warning.show_more": "إظهار المزيد",

View File

@@ -155,8 +155,6 @@
"confirmations.redraft.confirm": "Desaniciar y reeditar", "confirmations.redraft.confirm": "Desaniciar y reeditar",
"confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?", "confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
"confirmations.unfollow.confirm": "Dexar de siguir", "confirmations.unfollow.confirm": "Dexar de siguir",
"confirmations.unfollow.message": "¿De xuru que quies dexar de siguir a {name}?",
"confirmations.unfollow.title": "¿Dexar de siguir al usuariu?",
"content_warning.hide": "Esconder la publicación", "content_warning.hide": "Esconder la publicación",
"content_warning.show": "Amosar de toes toes", "content_warning.show": "Amosar de toes toes",
"content_warning.show_more": "Amosar más", "content_warning.show_more": "Amosar más",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.message": "Bu əməliyyatın geri dönüşü yoxdur.", "confirmations.revoke_quote.message": "Bu əməliyyatın geri dönüşü yoxdur.",
"confirmations.revoke_quote.title": "Göndəriş silinsin?", "confirmations.revoke_quote.title": "Göndəriş silinsin?",
"confirmations.unfollow.confirm": "İzləmədən çıxar", "confirmations.unfollow.confirm": "İzləmədən çıxar",
"confirmations.unfollow.message": "{name} izləmədən çıxmaq istədiyinizə əminsiniz?",
"confirmations.unfollow.title": "İstifadəçi izləmədən çıxarılsın?",
"content_warning.hide": "Paylaşımı gizlət", "content_warning.hide": "Paylaşımı gizlət",
"content_warning.show": "Yenə də göstər", "content_warning.show": "Yenə də göstər",
"content_warning.show_more": "Daha çox göstər", "content_warning.show_more": "Daha çox göstər",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.", "confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.",
"confirmations.revoke_quote.title": "Выдаліць допіс?", "confirmations.revoke_quote.title": "Выдаліць допіс?",
"confirmations.unfollow.confirm": "Адпісацца", "confirmations.unfollow.confirm": "Адпісацца",
"confirmations.unfollow.message": "Вы ўпэўненыя, што хочаце адпісацца ад {name}?",
"confirmations.unfollow.title": "Адпісацца ад карыстальніка?",
"content_warning.hide": "Схаваць допіс", "content_warning.hide": "Схаваць допіс",
"content_warning.show": "Усё адно паказаць", "content_warning.show": "Усё адно паказаць",
"content_warning.show_more": "Паказаць усё роўна", "content_warning.show_more": "Паказаць усё роўна",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.message": "Действието е неотменимо.", "confirmations.revoke_quote.message": "Действието е неотменимо.",
"confirmations.revoke_quote.title": "Премахвате ли публикацията?", "confirmations.revoke_quote.title": "Премахвате ли публикацията?",
"confirmations.unfollow.confirm": "Без следване", "confirmations.unfollow.confirm": "Без следване",
"confirmations.unfollow.message": "Наистина ли искате вече да не следвате {name}?",
"confirmations.unfollow.title": "Спирате ли да следвате потребителя?",
"content_warning.hide": "Скриване на публ.", "content_warning.hide": "Скриване на публ.",
"content_warning.show": "Нека се покаже", "content_warning.show": "Нека се покаже",
"content_warning.show_more": "Показване на още", "content_warning.show_more": "Показване на още",

View File

@@ -152,7 +152,6 @@
"confirmations.mute.confirm": "সরিয়ে ফেলুন", "confirmations.mute.confirm": "সরিয়ে ফেলুন",
"confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন", "confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন",
"confirmations.unfollow.confirm": "অনুসরণ বন্ধ করো", "confirmations.unfollow.confirm": "অনুসরণ বন্ধ করো",
"confirmations.unfollow.message": "তুমি কি নিশ্চিত {name} কে আর অনুসরণ করতে চাও না?",
"conversation.delete": "কথোপকথন মুছে ফেলুন", "conversation.delete": "কথোপকথন মুছে ফেলুন",
"conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন", "conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন",
"conversation.open": "কথপোকথন দেখান", "conversation.open": "কথপোকথন দেখান",

View File

@@ -217,8 +217,6 @@
"confirmations.revoke_quote.confirm": "Dilemel an embannadur", "confirmations.revoke_quote.confirm": "Dilemel an embannadur",
"confirmations.revoke_quote.title": "Dilemel an embannadur?", "confirmations.revoke_quote.title": "Dilemel an embannadur?",
"confirmations.unfollow.confirm": "Diheuliañ", "confirmations.unfollow.confirm": "Diheuliañ",
"confirmations.unfollow.message": "Ha sur oc'h e fell deoc'h paouez da heuliañ {name} ?",
"confirmations.unfollow.title": "Paouez da heuliañ an implijer·ez?",
"content_warning.hide": "Kuzhat an embannadur", "content_warning.hide": "Kuzhat an embannadur",
"content_warning.show": "Diskwel memes tra", "content_warning.show": "Diskwel memes tra",
"content_warning.show_more": "Diskouez muioc'h", "content_warning.show_more": "Diskouez muioc'h",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.message": "Aquesta acció no es pot desfer.", "confirmations.revoke_quote.message": "Aquesta acció no es pot desfer.",
"confirmations.revoke_quote.title": "Eliminar la publicació?", "confirmations.revoke_quote.title": "Eliminar la publicació?",
"confirmations.unfollow.confirm": "Deixa de seguir", "confirmations.unfollow.confirm": "Deixa de seguir",
"confirmations.unfollow.message": "Segur que vols deixar de seguir {name}?",
"confirmations.unfollow.title": "Deixar de seguir l'usuari?",
"content_warning.hide": "Amaga la publicació", "content_warning.hide": "Amaga la publicació",
"content_warning.show": "Mostra-la igualment", "content_warning.show": "Mostra-la igualment",
"content_warning.show_more": "Mostra'n més", "content_warning.show_more": "Mostra'n més",

View File

@@ -160,7 +160,6 @@
"confirmations.redraft.confirm": "سڕینەوە & دووبارە ڕەشکردنەوە", "confirmations.redraft.confirm": "سڕینەوە & دووبارە ڕەشکردنەوە",
"confirmations.redraft.message": "دڵنیای دەتەوێت ئەم پۆستە بسڕیتەوە و دووبارە دایبڕێژیتەوە؟ فەڤۆریت و بووستەکان لەدەست دەچن، وەڵامەکانی پۆستە ئەسڵیەکەش هەتیو دەبن.", "confirmations.redraft.message": "دڵنیای دەتەوێت ئەم پۆستە بسڕیتەوە و دووبارە دایبڕێژیتەوە؟ فەڤۆریت و بووستەکان لەدەست دەچن، وەڵامەکانی پۆستە ئەسڵیەکەش هەتیو دەبن.",
"confirmations.unfollow.confirm": "بەدوادانەچو", "confirmations.unfollow.confirm": "بەدوادانەچو",
"confirmations.unfollow.message": "ئایا دڵنیایت لەوەی دەتەوێت پەیڕەوی {name}?",
"conversation.delete": "سڕینەوەی گفتوگۆ", "conversation.delete": "سڕینەوەی گفتوگۆ",
"conversation.mark_as_read": "نیشانەکردن وەک خوێندراوە", "conversation.mark_as_read": "نیشانەکردن وەک خوێندراوە",
"conversation.open": "نیشاندان گفتوگۆ", "conversation.open": "نیشاندان گفتوگۆ",

View File

@@ -87,7 +87,6 @@
"confirmations.mute.confirm": "Piattà", "confirmations.mute.confirm": "Piattà",
"confirmations.redraft.confirm": "Sguassà è riscrive", "confirmations.redraft.confirm": "Sguassà è riscrive",
"confirmations.unfollow.confirm": "Disabbunassi", "confirmations.unfollow.confirm": "Disabbunassi",
"confirmations.unfollow.message": "Site sicuru·a ch'ùn vulete più siguità @{name}?",
"conversation.delete": "Sguassà a cunversazione", "conversation.delete": "Sguassà a cunversazione",
"conversation.mark_as_read": "Marcà cum'è lettu", "conversation.mark_as_read": "Marcà cum'è lettu",
"conversation.open": "Vede a cunversazione", "conversation.open": "Vede a cunversazione",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Tuto akci nelze vrátit zpět.", "confirmations.revoke_quote.message": "Tuto akci nelze vrátit zpět.",
"confirmations.revoke_quote.title": "Odstranit příspěvek?", "confirmations.revoke_quote.title": "Odstranit příspěvek?",
"confirmations.unfollow.confirm": "Přestat sledovat", "confirmations.unfollow.confirm": "Přestat sledovat",
"confirmations.unfollow.message": "Opravdu chcete {name} přestat sledovat?",
"confirmations.unfollow.title": "Přestat sledovat uživatele?",
"content_warning.hide": "Skrýt příspěvek", "content_warning.hide": "Skrýt příspěvek",
"content_warning.show": "Přesto zobrazit", "content_warning.show": "Přesto zobrazit",
"content_warning.show_more": "Zobrazit více", "content_warning.show_more": "Zobrazit více",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Does dim modd dadwneud y weithred hon.", "confirmations.revoke_quote.message": "Does dim modd dadwneud y weithred hon.",
"confirmations.revoke_quote.title": "Dileu'r postiad?", "confirmations.revoke_quote.title": "Dileu'r postiad?",
"confirmations.unfollow.confirm": "Dad-ddilyn", "confirmations.unfollow.confirm": "Dad-ddilyn",
"confirmations.unfollow.message": "Ydych chi'n siŵr eich bod am ddad-ddilyn {name}?",
"confirmations.unfollow.title": "Dad-ddilyn defnyddiwr?",
"content_warning.hide": "Cuddio'r postiad", "content_warning.hide": "Cuddio'r postiad",
"content_warning.show": "Dangos beth bynnag", "content_warning.show": "Dangos beth bynnag",
"content_warning.show_more": "Dangos rhagor", "content_warning.show_more": "Dangos rhagor",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Fjern indlæg", "confirmations.revoke_quote.confirm": "Fjern indlæg",
"confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.", "confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.",
"confirmations.revoke_quote.title": "Fjern indlæg?", "confirmations.revoke_quote.title": "Fjern indlæg?",
"confirmations.unblock.confirm": "Fjern blokering",
"confirmations.unblock.title": "Fjern blokering af {name}?",
"confirmations.unfollow.confirm": "Følg ikke længere", "confirmations.unfollow.confirm": "Følg ikke længere",
"confirmations.unfollow.message": "Er du sikker på, at du ikke længere vil følge {name}?", "confirmations.unfollow.title": "Følg ikke længere {name}?",
"confirmations.unfollow.title": "Følg ikke længere bruger?", "confirmations.withdraw_request.confirm": "Annullér anmodning",
"confirmations.withdraw_request.title": "Annullér anmodning om at følge {name}?",
"content_warning.hide": "Skjul indlæg", "content_warning.hide": "Skjul indlæg",
"content_warning.show": "Vis alligevel", "content_warning.show": "Vis alligevel",
"content_warning.show_more": "Vis flere", "content_warning.show_more": "Vis flere",
@@ -920,6 +923,8 @@
"status.quote_private": "Private indlæg kan ikke citeres", "status.quote_private": "Private indlæg kan ikke citeres",
"status.quotes": "{count, plural, one {citat} other {citater}}", "status.quotes": "{count, plural, one {citat} other {citater}}",
"status.quotes.empty": "Ingen har citeret dette indlæg endnu. Når det sker, vil det fremgå her.", "status.quotes.empty": "Ingen har citeret dette indlæg endnu. Når det sker, vil det fremgå her.",
"status.quotes.local_other_disclaimer": "Citater afvist af forfatteren vil ikke blive vist.",
"status.quotes.remote_other_disclaimer": "Kun citater fra {domain} vises med garanti her. Citater afvist af forfatteren vil ikke blive vist.",
"status.read_more": "Læs mere", "status.read_more": "Læs mere",
"status.reblog": "Fremhæv", "status.reblog": "Fremhæv",
"status.reblog_or_quote": "Fremhæv eller citér", "status.reblog_or_quote": "Fremhæv eller citér",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Beitrag entfernen", "confirmations.revoke_quote.confirm": "Beitrag entfernen",
"confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.", "confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.",
"confirmations.revoke_quote.title": "Beitrag entfernen?", "confirmations.revoke_quote.title": "Beitrag entfernen?",
"confirmations.unblock.confirm": "Nicht mehr blockieren",
"confirmations.unblock.title": "{name} nicht mehr blockieren?",
"confirmations.unfollow.confirm": "Entfolgen", "confirmations.unfollow.confirm": "Entfolgen",
"confirmations.unfollow.message": "Möchtest du {name} wirklich entfolgen?", "confirmations.unfollow.title": "{name} entfolgen?",
"confirmations.unfollow.title": "Profil entfolgen?", "confirmations.withdraw_request.confirm": "Anfrage zurückziehen",
"confirmations.withdraw_request.title": "Anfrage zum Folgen von {name} zurückziehen?",
"content_warning.hide": "Beitrag ausblenden", "content_warning.hide": "Beitrag ausblenden",
"content_warning.show": "Trotzdem anzeigen", "content_warning.show": "Trotzdem anzeigen",
"content_warning.show_more": "Beitrag anzeigen", "content_warning.show_more": "Beitrag anzeigen",
@@ -920,6 +923,8 @@
"status.quote_private": "Private Beiträge können nicht zitiert werden", "status.quote_private": "Private Beiträge können nicht zitiert werden",
"status.quotes": "{count, plural, one {Mal zitiert} other {Mal zitiert}}", "status.quotes": "{count, plural, one {Mal zitiert} other {Mal zitiert}}",
"status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.", "status.quotes.empty": "Diesen Beitrag hat bisher noch niemand zitiert. Sobald es jemand tut, wird das Profil hier erscheinen.",
"status.quotes.local_other_disclaimer": "Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
"status.quotes.remote_other_disclaimer": "Nur Zitate von {domain} werden hier garantiert angezeigt. Durch Autor*in abgelehnte Zitate werden nicht angezeigt.",
"status.read_more": "Gesamten Beitrag anschauen", "status.read_more": "Gesamten Beitrag anschauen",
"status.reblog": "Teilen", "status.reblog": "Teilen",
"status.reblog_or_quote": "Teilen oder zitieren", "status.reblog_or_quote": "Teilen oder zitieren",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Αφαίρεση ανάρτησης", "confirmations.revoke_quote.confirm": "Αφαίρεση ανάρτησης",
"confirmations.revoke_quote.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", "confirmations.revoke_quote.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"confirmations.revoke_quote.title": "Αφαίρεση ανάρτησης;", "confirmations.revoke_quote.title": "Αφαίρεση ανάρτησης;",
"confirmations.unblock.confirm": "Άρση αποκλεισμού",
"confirmations.unblock.title": "Άρση αποκλεισμού {name};",
"confirmations.unfollow.confirm": "Άρση ακολούθησης", "confirmations.unfollow.confirm": "Άρση ακολούθησης",
"confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};", "confirmations.unfollow.title": "Κατάργηση ακολούθησης του/της {name};",
"confirmations.unfollow.title": "Άρση ακολούθησης;", "confirmations.withdraw_request.confirm": "Απόσυρση αιτήματος",
"confirmations.withdraw_request.title": "Απόσυρση αιτήματος για να ακολουθήσετε τον/την {name};",
"content_warning.hide": "Απόκρυψη ανάρτησης", "content_warning.hide": "Απόκρυψη ανάρτησης",
"content_warning.show": "Εμφάνιση ούτως ή άλλως", "content_warning.show": "Εμφάνιση ούτως ή άλλως",
"content_warning.show_more": "Εμφάνιση περισσότερων", "content_warning.show_more": "Εμφάνιση περισσότερων",
@@ -920,6 +923,8 @@
"status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν", "status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν",
"status.quotes": "{count, plural, one {# παράθεση} other {# παραθέσεις}}", "status.quotes": "{count, plural, one {# παράθεση} other {# παραθέσεις}}",
"status.quotes.empty": "Κανείς δεν έχει παραθέσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει, θα εμφανιστεί εδώ.", "status.quotes.empty": "Κανείς δεν έχει παραθέσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει, θα εμφανιστεί εδώ.",
"status.quotes.local_other_disclaimer": "Οι παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.",
"status.quotes.remote_other_disclaimer": "Μόνο παραθέσεις από το {domain} είναι εγγυημένες ότι θα εμφανιστούν εδώ. Παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.",
"status.read_more": "Διάβασε περισότερα", "status.read_more": "Διάβασε περισότερα",
"status.reblog": "Ενίσχυση", "status.reblog": "Ενίσχυση",
"status.reblog_or_quote": "Ενίσχυση ή παράθεση", "status.reblog_or_quote": "Ενίσχυση ή παράθεση",

View File

@@ -245,8 +245,6 @@
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?", "confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
"confirmations.remove_from_followers.title": "Remove follower?", "confirmations.remove_from_followers.title": "Remove follower?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?",
"content_warning.hide": "Hide post", "content_warning.hide": "Hide post",
"content_warning.show": "Show anyway", "content_warning.show": "Show anyway",
"content_warning.show_more": "Show more", "content_warning.show_more": "Show more",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Remove post", "confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.", "confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?", "confirmations.revoke_quote.title": "Remove post?",
"confirmations.unblock.confirm": "Unblock",
"confirmations.unblock.title": "Unblock {name}?",
"confirmations.unfollow.confirm": "Unfollow", "confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?", "confirmations.unfollow.title": "Unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?", "confirmations.withdraw_request.confirm": "Withdraw request",
"confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
"content_warning.hide": "Hide post", "content_warning.hide": "Hide post",
"content_warning.show": "Show anyway", "content_warning.show": "Show anyway",
"content_warning.show_more": "Show more", "content_warning.show_more": "Show more",
@@ -920,6 +923,8 @@
"status.quote_private": "Private posts cannot be quoted", "status.quote_private": "Private posts cannot be quoted",
"status.quotes": "{count, plural, one {quote} other {quotes}}", "status.quotes": "{count, plural, one {quote} other {quotes}}",
"status.quotes.empty": "No one has quoted this post yet. When someone does, it will show up here.", "status.quotes.empty": "No one has quoted this post yet. When someone does, it will show up here.",
"status.quotes.local_other_disclaimer": "Quotes rejected by the author will not be shown.",
"status.quotes.remote_other_disclaimer": "Only quotes from {domain} are guaranteed to be shown here. Quotes rejected by the author will not be shown.",
"status.read_more": "Read more", "status.read_more": "Read more",
"status.reblog": "Boost", "status.reblog": "Boost",
"status.reblog_or_quote": "Boost or quote", "status.reblog_or_quote": "Boost or quote",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.message": "Ĉi tiu ago ne povas esti malfarita.", "confirmations.revoke_quote.message": "Ĉi tiu ago ne povas esti malfarita.",
"confirmations.revoke_quote.title": "Ĉu forigi afiŝon?", "confirmations.revoke_quote.title": "Ĉu forigi afiŝon?",
"confirmations.unfollow.confirm": "Ne plu sekvi", "confirmations.unfollow.confirm": "Ne plu sekvi",
"confirmations.unfollow.message": "Ĉu vi certas, ke vi volas ĉesi sekvi {name}?",
"confirmations.unfollow.title": "Ĉu ĉesi sekvi uzanton?",
"content_warning.hide": "Kaŝi afiŝon", "content_warning.hide": "Kaŝi afiŝon",
"content_warning.show": "Montri ĉiukaze", "content_warning.show": "Montri ĉiukaze",
"content_warning.show_more": "Montri pli", "content_warning.show_more": "Montri pli",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Eliminar mensaje", "confirmations.revoke_quote.confirm": "Eliminar mensaje",
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
"confirmations.revoke_quote.title": "¿Eliminar mensaje?", "confirmations.revoke_quote.title": "¿Eliminar mensaje?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "¿Desbloquear a {name}?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro que querés dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.withdraw_request.confirm": "Retirar solicitud",
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
"content_warning.hide": "Ocultar mensaje", "content_warning.hide": "Ocultar mensaje",
"content_warning.show": "Mostrar de todos modos", "content_warning.show": "Mostrar de todos modos",
"content_warning.show_more": "Mostrar más", "content_warning.show_more": "Mostrar más",
@@ -920,6 +923,8 @@
"status.quote_private": "No se pueden citar los mensajes privados", "status.quote_private": "No se pueden citar los mensajes privados",
"status.quotes": "{count, plural, one {# voto} other {# votos}}", "status.quotes": "{count, plural, one {# voto} other {# votos}}",
"status.quotes.empty": "Todavía nadie citó este mensaje. Cuando alguien lo haga, se mostrará acá.", "status.quotes.empty": "Todavía nadie citó este mensaje. Cuando alguien lo haga, se mostrará acá.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no se mostrarán.",
"status.quotes.remote_other_disclaimer": "Solo se muestran las citas de {domain}. Las citas rechazadas por el autor no se mostrarán.",
"status.read_more": "Leé más", "status.read_more": "Leé más",
"status.reblog": "Adherir", "status.reblog": "Adherir",
"status.reblog_or_quote": "Adherir o citar", "status.reblog_or_quote": "Adherir o citar",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.", "confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
"confirmations.revoke_quote.title": "¿Deseas eliminar la publicación?", "confirmations.revoke_quote.title": "¿Deseas eliminar la publicación?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "¿Desbloquear a {name}?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Estás seguro de que quieres dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.withdraw_request.confirm": "Retirar solicitud",
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
"content_warning.hide": "Ocultar publicación", "content_warning.hide": "Ocultar publicación",
"content_warning.show": "Mostrar de todos modos", "content_warning.show": "Mostrar de todos modos",
"content_warning.show_more": "Mostrar más", "content_warning.show_more": "Mostrar más",
@@ -920,6 +923,8 @@
"status.quote_private": "Las publicaciones privadas no pueden citarse", "status.quote_private": "Las publicaciones privadas no pueden citarse",
"status.quotes": "{count, plural,one {cita} other {citas}}", "status.quotes": "{count, plural,one {cita} other {citas}}",
"status.quotes.empty": "Nadie ha citado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.", "status.quotes.empty": "Nadie ha citado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no se mostrarán.",
"status.quotes.remote_other_disclaimer": "Solo se muestran las citas de {domain}. Las citas rechazadas por el autor no se mostrarán.",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_or_quote": "Impulsar o citar", "status.reblog_or_quote": "Impulsar o citar",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.", "confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.",
"confirmations.revoke_quote.title": "¿Eliminar la publicación?", "confirmations.revoke_quote.title": "¿Eliminar la publicación?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "¿Desbloquear a {name}?",
"confirmations.unfollow.confirm": "Dejar de seguir", "confirmations.unfollow.confirm": "Dejar de seguir",
"confirmations.unfollow.message": "¿Seguro que quieres dejar de seguir a {name}?", "confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?", "confirmations.withdraw_request.confirm": "Retirar solicitud",
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
"content_warning.hide": "Ocultar publicación", "content_warning.hide": "Ocultar publicación",
"content_warning.show": "Mostrar de todos modos", "content_warning.show": "Mostrar de todos modos",
"content_warning.show_more": "Mostrar más", "content_warning.show_more": "Mostrar más",
@@ -920,6 +923,8 @@
"status.quote_private": "Las publicaciones privadas no pueden ser citadas", "status.quote_private": "Las publicaciones privadas no pueden ser citadas",
"status.quotes": "{count, plural,one {cita} other {citas}}", "status.quotes": "{count, plural,one {cita} other {citas}}",
"status.quotes.empty": "Nadie ha citado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.", "status.quotes.empty": "Nadie ha citado esta publicación todavía. Cuando alguien lo haga, aparecerá aquí.",
"status.quotes.local_other_disclaimer": "Las citas rechazadas por el autor no se mostrarán.",
"status.quotes.remote_other_disclaimer": "Solo se muestran las citas de {domain}. Las citas rechazadas por el autor no se mostrarán.",
"status.read_more": "Leer más", "status.read_more": "Leer más",
"status.reblog": "Impulsar", "status.reblog": "Impulsar",
"status.reblog_or_quote": "Impulsar o citar", "status.reblog_or_quote": "Impulsar o citar",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Seda tegevust ei saa tagasi pöörata.", "confirmations.revoke_quote.message": "Seda tegevust ei saa tagasi pöörata.",
"confirmations.revoke_quote.title": "Kas eemaldame postituse?", "confirmations.revoke_quote.title": "Kas eemaldame postituse?",
"confirmations.unfollow.confirm": "Ära jälgi", "confirmations.unfollow.confirm": "Ära jälgi",
"confirmations.unfollow.message": "Oled kindel, et ei soovi rohkem jälgida kasutajat {name}?",
"confirmations.unfollow.title": "Ei jälgi enam kasutajat?",
"content_warning.hide": "Peida postitus", "content_warning.hide": "Peida postitus",
"content_warning.show": "Näita ikkagi", "content_warning.show": "Näita ikkagi",
"content_warning.show_more": "Näita rohkem", "content_warning.show_more": "Näita rohkem",

View File

@@ -240,8 +240,6 @@
"confirmations.remove_from_followers.message": "{name}-k zu jarraitzeari utziko dio. Seguru zaude jarraitu nahi duzula?", "confirmations.remove_from_followers.message": "{name}-k zu jarraitzeari utziko dio. Seguru zaude jarraitu nahi duzula?",
"confirmations.remove_from_followers.title": "Jarraitzailea kendu nahi duzu?", "confirmations.remove_from_followers.title": "Jarraitzailea kendu nahi duzu?",
"confirmations.unfollow.confirm": "Utzi jarraitzeari", "confirmations.unfollow.confirm": "Utzi jarraitzeari",
"confirmations.unfollow.message": "Ziur {name} jarraitzeari utzi nahi diozula?",
"confirmations.unfollow.title": "Erabiltzailea jarraitzeari utzi?",
"content_warning.hide": "Tuta ezkutatu", "content_warning.hide": "Tuta ezkutatu",
"content_warning.show": "Erakutsi hala ere", "content_warning.show": "Erakutsi hala ere",
"content_warning.show_more": "Erakutsi gehiago", "content_warning.show_more": "Erakutsi gehiago",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.", "confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.",
"confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟", "confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟",
"confirmations.unfollow.confirm": "پی‌نگرفتن", "confirmations.unfollow.confirm": "پی‌نگرفتن",
"confirmations.unfollow.message": "مطمئنید که می‌خواهید به پی‌گیری از {name} پایان دهید؟",
"confirmations.unfollow.title": "ناپی‌گیری کاربر؟",
"content_warning.hide": "نهفتن فرسته", "content_warning.hide": "نهفتن فرسته",
"content_warning.show": "در هر صورت نشان داده شود", "content_warning.show": "در هر صورت نشان داده شود",
"content_warning.show_more": "نمایش بیش‌تر", "content_warning.show_more": "نمایش بیش‌تر",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Poista julkaisu", "confirmations.revoke_quote.confirm": "Poista julkaisu",
"confirmations.revoke_quote.message": "Tätä toimea ei voi peruuttaa.", "confirmations.revoke_quote.message": "Tätä toimea ei voi peruuttaa.",
"confirmations.revoke_quote.title": "Poistetaanko julkaisu?", "confirmations.revoke_quote.title": "Poistetaanko julkaisu?",
"confirmations.unblock.confirm": "Kumoa esto",
"confirmations.unblock.title": "Kumotaanko käyttäjän {name} esto?",
"confirmations.unfollow.confirm": "Lopeta seuraaminen", "confirmations.unfollow.confirm": "Lopeta seuraaminen",
"confirmations.unfollow.message": "Haluatko varmasti lopettaa profiilin {name} seuraamisen?", "confirmations.unfollow.title": "Lopetetaanko käyttäjän {name} seuraaminen?",
"confirmations.unfollow.title": "Lopetetaanko käyttäjän seuraaminen?", "confirmations.withdraw_request.confirm": "Peruuta pyyntö",
"confirmations.withdraw_request.title": "Peruutetaanko pyyntö seurata käyttäjää {name}?",
"content_warning.hide": "Piilota julkaisu", "content_warning.hide": "Piilota julkaisu",
"content_warning.show": "Näytä kuitenkin", "content_warning.show": "Näytä kuitenkin",
"content_warning.show_more": "Näytä lisää", "content_warning.show_more": "Näytä lisää",
@@ -920,6 +923,8 @@
"status.quote_private": "Yksityisiä julkaisuja ei voi lainata", "status.quote_private": "Yksityisiä julkaisuja ei voi lainata",
"status.quotes": "{count, plural, one {lainaus} other {lainausta}}", "status.quotes": "{count, plural, one {lainaus} other {lainausta}}",
"status.quotes.empty": "Kukaan ei ole vielä lainannut tätä julkaisua. Kun joku tekee niin, se tulee tähän näkyviin.", "status.quotes.empty": "Kukaan ei ole vielä lainannut tätä julkaisua. Kun joku tekee niin, se tulee tähän näkyviin.",
"status.quotes.local_other_disclaimer": "Tekijän hylkäämiä lainauksia ei näytetä.",
"status.quotes.remote_other_disclaimer": "Vain palvelimen {domain} lainaukset näkyvät taatusti tässä. Tekijän hylkäämiä lainauksia ei näytetä.",
"status.read_more": "Näytä enemmän", "status.read_more": "Näytä enemmän",
"status.reblog": "Tehosta", "status.reblog": "Tehosta",
"status.reblog_or_quote": "Tehosta tai lainaa", "status.reblog_or_quote": "Tehosta tai lainaa",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Hendan atgerðin kann ikki angrast.", "confirmations.revoke_quote.message": "Hendan atgerðin kann ikki angrast.",
"confirmations.revoke_quote.title": "Strika post?", "confirmations.revoke_quote.title": "Strika post?",
"confirmations.unfollow.confirm": "Fylg ikki", "confirmations.unfollow.confirm": "Fylg ikki",
"confirmations.unfollow.message": "Ert tú vís/ur í, at tú vil steðga við at fylgja {name}?",
"confirmations.unfollow.title": "Gevst at fylgja brúkara?",
"content_warning.hide": "Fjal post", "content_warning.hide": "Fjal post",
"content_warning.show": "Vís kortini", "content_warning.show": "Vís kortini",
"content_warning.show_more": "Vís meiri", "content_warning.show_more": "Vís meiri",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.", "confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
"confirmations.revoke_quote.title": "Retirer la publication ?", "confirmations.revoke_quote.title": "Retirer la publication ?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Voulez-vous vraiment arrêter de suivre {name}?",
"confirmations.unfollow.title": "Se désabonner de l'utilisateur·rice ?",
"content_warning.hide": "Masquer le message", "content_warning.hide": "Masquer le message",
"content_warning.show": "Montrer quand même", "content_warning.show": "Montrer quand même",
"content_warning.show_more": "Montrer plus", "content_warning.show_more": "Montrer plus",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.", "confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
"confirmations.revoke_quote.title": "Retirer la publication ?", "confirmations.revoke_quote.title": "Retirer la publication ?",
"confirmations.unfollow.confirm": "Ne plus suivre", "confirmations.unfollow.confirm": "Ne plus suivre",
"confirmations.unfollow.message": "Voulez-vous vraiment vous désabonner de {name}?",
"confirmations.unfollow.title": "Se désabonner de l'utilisateur·rice ?",
"content_warning.hide": "Masquer le message", "content_warning.hide": "Masquer le message",
"content_warning.show": "Montrer quand même", "content_warning.show": "Montrer quand même",
"content_warning.show_more": "Montrer plus", "content_warning.show_more": "Montrer plus",

View File

@@ -245,8 +245,6 @@
"confirmations.remove_from_followers.message": "{name} sil jo net mear folgje. Binne jo wis dat jo trochgean wolle?", "confirmations.remove_from_followers.message": "{name} sil jo net mear folgje. Binne jo wis dat jo trochgean wolle?",
"confirmations.remove_from_followers.title": "Folger fuortsmite?", "confirmations.remove_from_followers.title": "Folger fuortsmite?",
"confirmations.unfollow.confirm": "Net mear folgje", "confirmations.unfollow.confirm": "Net mear folgje",
"confirmations.unfollow.message": "Binne jo wis dat jo {name} net mear folgje wolle?",
"confirmations.unfollow.title": "Brûker net mear folgje?",
"content_warning.hide": "Berjocht ferstopje", "content_warning.hide": "Berjocht ferstopje",
"content_warning.show": "Dochs toane", "content_warning.show": "Dochs toane",
"content_warning.show_more": "Mear toane", "content_warning.show_more": "Mear toane",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Ní féidir an gníomh seo a chealú.", "confirmations.revoke_quote.message": "Ní féidir an gníomh seo a chealú.",
"confirmations.revoke_quote.title": "Bain postáil?", "confirmations.revoke_quote.title": "Bain postáil?",
"confirmations.unfollow.confirm": "Ná lean", "confirmations.unfollow.confirm": "Ná lean",
"confirmations.unfollow.message": "An bhfuil tú cinnte gur mhaith leat {name} a dhíleanúint?",
"confirmations.unfollow.title": "Dílean an t-úsáideoir?",
"content_warning.hide": "Folaigh postáil", "content_warning.hide": "Folaigh postáil",
"content_warning.show": "Taispeáin ar aon nós", "content_warning.show": "Taispeáin ar aon nós",
"content_warning.show_more": "Taispeáin níos mó", "content_warning.show_more": "Taispeáin níos mó",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "Cha ghabh seo a neo-dhèanamh.", "confirmations.revoke_quote.message": "Cha ghabh seo a neo-dhèanamh.",
"confirmations.revoke_quote.title": "A bheil thu airson am post a thoirt air falbh?", "confirmations.revoke_quote.title": "A bheil thu airson am post a thoirt air falbh?",
"confirmations.unfollow.confirm": "Na lean tuilleadh", "confirmations.unfollow.confirm": "Na lean tuilleadh",
"confirmations.unfollow.message": "A bheil thu cinnteach nach eil thu airson {name} a leantainn tuilleadh?",
"confirmations.unfollow.title": "A bheil thu airson sgur de leantainn a chleachdaiche?",
"content_warning.hide": "Falaich am post", "content_warning.hide": "Falaich am post",
"content_warning.show": "Seall e co-dhiù", "content_warning.show": "Seall e co-dhiù",
"content_warning.show_more": "Seall barrachd dheth", "content_warning.show_more": "Seall barrachd dheth",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Eliminar publicación", "confirmations.revoke_quote.confirm": "Eliminar publicación",
"confirmations.revoke_quote.message": "Esta acción non se pode desfacer.", "confirmations.revoke_quote.message": "Esta acción non se pode desfacer.",
"confirmations.revoke_quote.title": "Eliminar publicación?", "confirmations.revoke_quote.title": "Eliminar publicación?",
"confirmations.unblock.confirm": "Desbloquear",
"confirmations.unblock.title": "Desbloquear a {name}?",
"confirmations.unfollow.confirm": "Deixar de seguir", "confirmations.unfollow.confirm": "Deixar de seguir",
"confirmations.unfollow.message": "Tes certeza de querer deixar de seguir a {name}?", "confirmations.unfollow.title": "Deixa de seguir a {name}?",
"confirmations.unfollow.title": "Deixar de seguir á usuaria?", "confirmations.withdraw_request.confirm": "Retirar solicitude",
"confirmations.withdraw_request.title": "Retirar a petición de seguimento para {name}?",
"content_warning.hide": "Agochar publicación", "content_warning.hide": "Agochar publicación",
"content_warning.show": "Mostrar igualmente", "content_warning.show": "Mostrar igualmente",
"content_warning.show_more": "Mostrar máis", "content_warning.show_more": "Mostrar máis",
@@ -920,6 +923,8 @@
"status.quote_private": "As publicacións privadas non se poden citar", "status.quote_private": "As publicacións privadas non se poden citar",
"status.quotes": "{count, plural, one {cita} other {citas}}", "status.quotes": "{count, plural, one {cita} other {citas}}",
"status.quotes.empty": "Aínda ninguén citou esta publicación. Cando alguén o faga aparecerá aquí.", "status.quotes.empty": "Aínda ninguén citou esta publicación. Cando alguén o faga aparecerá aquí.",
"status.quotes.local_other_disclaimer": "Non se mostrarán as citas rexeitadas pola autora.",
"status.quotes.remote_other_disclaimer": "Só se garante que se mostren as citas do dominio {domain}. Non se mostrarán as citas rexeitadas pola persoa autora.",
"status.read_more": "Ler máis", "status.read_more": "Ler máis",
"status.reblog": "Promover", "status.reblog": "Promover",
"status.reblog_or_quote": "Promover ou citar", "status.reblog_or_quote": "Promover ou citar",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "הסרת הודעה", "confirmations.revoke_quote.confirm": "הסרת הודעה",
"confirmations.revoke_quote.message": "פעולה זו אינה הפיכה.", "confirmations.revoke_quote.message": "פעולה זו אינה הפיכה.",
"confirmations.revoke_quote.title": "הסרת הודעה?", "confirmations.revoke_quote.title": "הסרת הודעה?",
"confirmations.unblock.confirm": "הסרת חסימה",
"confirmations.unblock.title": "הסרת חסימה מ־{name}?",
"confirmations.unfollow.confirm": "הפסקת מעקב", "confirmations.unfollow.confirm": "הפסקת מעקב",
"confirmations.unfollow.message": הפסיק מעקב אחרי {name}?", "confirmations.unfollow.title": "בטול מעקב אחרי {name}?",
"confirmations.unfollow.title": "לבטל מעקב אחר המשתמש.ת?", "confirmations.withdraw_request.confirm": "משיכת בקשה",
"confirmations.withdraw_request.title": "משיכת בקשת מעקב אחרי {name}?",
"content_warning.hide": "הסתרת חיצרוץ", "content_warning.hide": "הסתרת חיצרוץ",
"content_warning.show": "להציג בכל זאת", "content_warning.show": "להציג בכל זאת",
"content_warning.show_more": "הצג עוד", "content_warning.show_more": "הצג עוד",
@@ -920,6 +923,8 @@
"status.quote_private": "הודעות פרטיות לא ניתנות לציטוט", "status.quote_private": "הודעות פרטיות לא ניתנות לציטוט",
"status.quotes": "{count, plural,one {ציטוט}other {ציטוטים}}", "status.quotes": "{count, plural,one {ציטוט}other {ציטוטים}}",
"status.quotes.empty": "עוד לא ציטטו את ההודעה הזו. כאשר זה יקרה, הציטוטים יופיעו כאן.", "status.quotes.empty": "עוד לא ציטטו את ההודעה הזו. כאשר זה יקרה, הציטוטים יופיעו כאן.",
"status.quotes.local_other_disclaimer": "ציטוטים שיידחו על ידי המחברים המקוריים לא יוצגו.",
"status.quotes.remote_other_disclaimer": "רק ציטוטים מהשרת {domain} מובטחים שיופיעו פה. ציטוטים שנדחו על ידי המצוטטים לא יופיעו.",
"status.read_more": "לקרוא עוד", "status.read_more": "לקרוא עוד",
"status.reblog": "הדהוד", "status.reblog": "הדהוד",
"status.reblog_or_quote": "להדהד או לצטט", "status.reblog_or_quote": "להדהד או לצטט",

View File

@@ -168,7 +168,6 @@
"confirmations.redraft.confirm": "मिटायें और पुनःप्रारूपण करें", "confirmations.redraft.confirm": "मिटायें और पुनःप्रारूपण करें",
"confirmations.redraft.message": "क्या आप वाकई इस स्टेटस को हटाना चाहते हैं और इसे फिर से ड्राफ्ट करना चाहते हैं? पसंदीदा और बूस्ट खो जाएंगे, और मूल पोस्ट के उत्तर अनाथ हो जाएंगे।", "confirmations.redraft.message": "क्या आप वाकई इस स्टेटस को हटाना चाहते हैं और इसे फिर से ड्राफ्ट करना चाहते हैं? पसंदीदा और बूस्ट खो जाएंगे, और मूल पोस्ट के उत्तर अनाथ हो जाएंगे।",
"confirmations.unfollow.confirm": "अनफॉलो करें", "confirmations.unfollow.confirm": "अनफॉलो करें",
"confirmations.unfollow.message": "क्या आप वाकई {name} को अनफॉलो करना चाहते हैं?",
"conversation.delete": "वार्तालाप हटाएँ", "conversation.delete": "वार्तालाप हटाएँ",
"conversation.mark_as_read": "पढ़ा गया के रूप में चिह्नित करें", "conversation.mark_as_read": "पढ़ा गया के रूप में चिह्नित करें",
"conversation.open": "वार्तालाप देखें", "conversation.open": "वार्तालाप देखें",

View File

@@ -146,7 +146,6 @@
"confirmations.mute.confirm": "Utišaj", "confirmations.mute.confirm": "Utišaj",
"confirmations.redraft.confirm": "Izbriši i ponovno uredi", "confirmations.redraft.confirm": "Izbriši i ponovno uredi",
"confirmations.unfollow.confirm": "Prestani pratiti", "confirmations.unfollow.confirm": "Prestani pratiti",
"confirmations.unfollow.message": "Jeste li sigurni da želite prestati pratiti {name}?",
"conversation.delete": "Izbriši razgovor", "conversation.delete": "Izbriši razgovor",
"conversation.mark_as_read": "Označi kao pročitano", "conversation.mark_as_read": "Označi kao pročitano",
"conversation.open": "Prikaži razgovor", "conversation.open": "Prikaži razgovor",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "Ez a művelet nem vonható vissza.", "confirmations.revoke_quote.message": "Ez a művelet nem vonható vissza.",
"confirmations.revoke_quote.title": "Bejegyzés eltávolítása?", "confirmations.revoke_quote.title": "Bejegyzés eltávolítása?",
"confirmations.unfollow.confirm": "Követés visszavonása", "confirmations.unfollow.confirm": "Követés visszavonása",
"confirmations.unfollow.message": "Biztos, hogy vissza szeretnéd vonni {name} követését?",
"confirmations.unfollow.title": "Megszünteted a felhasználó követését?",
"content_warning.hide": "Bejegyzés elrejtése", "content_warning.hide": "Bejegyzés elrejtése",
"content_warning.show": "Megjelenítés mindenképp", "content_warning.show": "Megjelenítés mindenképp",
"content_warning.show_more": "Több megjelenítése", "content_warning.show_more": "Több megjelenítése",

View File

@@ -124,7 +124,6 @@
"confirmations.mute.confirm": "Լռեցնել", "confirmations.mute.confirm": "Լռեցնել",
"confirmations.redraft.confirm": "Ջնջել եւ խմբագրել նորից", "confirmations.redraft.confirm": "Ջնջել եւ խմբագրել նորից",
"confirmations.unfollow.confirm": "Ապահետեւել", "confirmations.unfollow.confirm": "Ապահետեւել",
"confirmations.unfollow.message": "Վստա՞հ ես, որ ուզում ես այլեւս չհետեւել {name}֊ին։",
"conversation.delete": "Ջնջել խօսակցութիւնը", "conversation.delete": "Ջնջել խօսակցութիւնը",
"conversation.mark_as_read": "Նշել որպէս ընթերցուած", "conversation.mark_as_read": "Նշել որպէս ընթերցուած",
"conversation.open": "Դիտել խօսակցութիւնը", "conversation.open": "Դիտել խօսակցութիւնը",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Iste action non pote esser disfacite.", "confirmations.revoke_quote.message": "Iste action non pote esser disfacite.",
"confirmations.revoke_quote.title": "Remover message?", "confirmations.revoke_quote.title": "Remover message?",
"confirmations.unfollow.confirm": "Non plus sequer", "confirmations.unfollow.confirm": "Non plus sequer",
"confirmations.unfollow.message": "Es tu secur que tu vole cessar de sequer {name}?",
"confirmations.unfollow.title": "Cessar de sequer le usator?",
"content_warning.hide": "Celar le message", "content_warning.hide": "Celar le message",
"content_warning.show": "Monstrar in omne caso", "content_warning.show": "Monstrar in omne caso",
"content_warning.show_more": "Monstrar plus", "content_warning.show_more": "Monstrar plus",

View File

@@ -189,8 +189,6 @@
"confirmations.redraft.message": "Apakah anda yakin ingin menghapus postingan ini dan menyusun ulang postingan ini? Favorit dan peningkatan akan hilang, dan balasan ke postingan asli tidak akan terhubung ke postingan manapun.", "confirmations.redraft.message": "Apakah anda yakin ingin menghapus postingan ini dan menyusun ulang postingan ini? Favorit dan peningkatan akan hilang, dan balasan ke postingan asli tidak akan terhubung ke postingan manapun.",
"confirmations.redraft.title": "Delete & redraft post?", "confirmations.redraft.title": "Delete & redraft post?",
"confirmations.unfollow.confirm": "Berhenti mengikuti", "confirmations.unfollow.confirm": "Berhenti mengikuti",
"confirmations.unfollow.message": "Apakah Anda ingin berhenti mengikuti {name}?",
"confirmations.unfollow.title": "Unfollow user?",
"content_warning.hide": "Hide post", "content_warning.hide": "Hide post",
"content_warning.show": "Show anyway", "content_warning.show": "Show anyway",
"conversation.delete": "Hapus percakapan", "conversation.delete": "Hapus percakapan",

View File

@@ -168,7 +168,6 @@
"confirmations.redraft.confirm": "Deleter & redacter", "confirmations.redraft.confirm": "Deleter & redacter",
"confirmations.redraft.message": "Esque tu vermen vole deleter ti-ci posta e redacter it? Favorites e boosts va esser perdit, e responses al posta original va esser orfanat.", "confirmations.redraft.message": "Esque tu vermen vole deleter ti-ci posta e redacter it? Favorites e boosts va esser perdit, e responses al posta original va esser orfanat.",
"confirmations.unfollow.confirm": "Dessequer", "confirmations.unfollow.confirm": "Dessequer",
"confirmations.unfollow.message": "Esque tu vermen vole dessequer {name}?",
"conversation.delete": "Deleter conversation", "conversation.delete": "Deleter conversation",
"conversation.mark_as_read": "Marcar quam leet", "conversation.mark_as_read": "Marcar quam leet",
"conversation.open": "Vider conversation", "conversation.open": "Vider conversation",

View File

@@ -218,8 +218,6 @@
"confirmations.redraft.message": "Ka vu certe volas efacar ca posto e riskisigar ol? Favoriziti e repeti esos perdita, e respondi al posto originala esos orfanigita.", "confirmations.redraft.message": "Ka vu certe volas efacar ca posto e riskisigar ol? Favoriziti e repeti esos perdita, e respondi al posto originala esos orfanigita.",
"confirmations.redraft.title": "Ka efacar & riskisar posto?", "confirmations.redraft.title": "Ka efacar & riskisar posto?",
"confirmations.unfollow.confirm": "Desequez", "confirmations.unfollow.confirm": "Desequez",
"confirmations.unfollow.message": "Ka vu certe volas desequar {name}?",
"confirmations.unfollow.title": "Ka dessequar uzanto?",
"content_warning.hide": "Celez posto", "content_warning.hide": "Celez posto",
"content_warning.show": "Montrez nur", "content_warning.show": "Montrez nur",
"content_warning.show_more": "Montrar plu", "content_warning.show_more": "Montrar plu",

View File

@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Fjarlægja færslu", "confirmations.revoke_quote.confirm": "Fjarlægja færslu",
"confirmations.revoke_quote.message": "Þessa aðgerð er ekki hægt að afturkalla.", "confirmations.revoke_quote.message": "Þessa aðgerð er ekki hægt að afturkalla.",
"confirmations.revoke_quote.title": "Fjarlægja færslu?", "confirmations.revoke_quote.title": "Fjarlægja færslu?",
"confirmations.unblock.confirm": "Aflétta útilokun",
"confirmations.unblock.title": "Aflétta útilokun á {name}?",
"confirmations.unfollow.confirm": "Hætta að fylgja", "confirmations.unfollow.confirm": "Hætta að fylgja",
"confirmations.unfollow.message": "Ertu viss um að þú viljir hætta að fylgjast með {name}?", "confirmations.unfollow.title": "Hætta að fylgjast með {name}?",
"confirmations.unfollow.title": "Hætta að fylgjast með viðkomandi?", "confirmations.withdraw_request.confirm": "Taka beiðni til baka",
"confirmations.withdraw_request.title": "Taka aftur beiðni um að fylgjast með {name}?",
"content_warning.hide": "Fela færslu", "content_warning.hide": "Fela færslu",
"content_warning.show": "Birta samt", "content_warning.show": "Birta samt",
"content_warning.show_more": "Sýna meira", "content_warning.show_more": "Sýna meira",
@@ -920,6 +923,8 @@
"status.quote_private": "Ekki er hægt að vitna í einkafærslur", "status.quote_private": "Ekki er hægt að vitna í einkafærslur",
"status.quotes": "{count, plural, one {tilvitnun} other {tilvitnanir}}", "status.quotes": "{count, plural, one {tilvitnun} other {tilvitnanir}}",
"status.quotes.empty": "Enginn hefur ennþá vitnað í þessa færslu. Þegar einhver gerir það, mun það birtast hér.", "status.quotes.empty": "Enginn hefur ennþá vitnað í þessa færslu. Þegar einhver gerir það, mun það birtast hér.",
"status.quotes.local_other_disclaimer": "Tilvitnanir sem höfundur hafnar verða ekki birtar.",
"status.quotes.remote_other_disclaimer": "Aðeins tilvitnanir frá {domain} munu birtast hér. Tilvitnanir sem höfundur hafnar verða ekki birtar.",
"status.read_more": "Lesa meira", "status.read_more": "Lesa meira",
"status.reblog": "Endurbirting", "status.reblog": "Endurbirting",
"status.reblog_or_quote": "Endurbirta eða vitna í færslu", "status.reblog_or_quote": "Endurbirta eða vitna í færslu",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "Questa azione non può essere annullata.", "confirmations.revoke_quote.message": "Questa azione non può essere annullata.",
"confirmations.revoke_quote.title": "Rimuovere il post?", "confirmations.revoke_quote.title": "Rimuovere il post?",
"confirmations.unfollow.confirm": "Smetti di seguire", "confirmations.unfollow.confirm": "Smetti di seguire",
"confirmations.unfollow.message": "Sei sicuro di voler smettere di seguire {name}?",
"confirmations.unfollow.title": "Smettere di seguire l'utente?",
"content_warning.hide": "Nascondi post", "content_warning.hide": "Nascondi post",
"content_warning.show": "Mostra comunque", "content_warning.show": "Mostra comunque",
"content_warning.show_more": "Mostra di più", "content_warning.show_more": "Mostra di più",

View File

@@ -248,8 +248,6 @@
"confirmations.revoke_quote.confirm": "投稿を削除", "confirmations.revoke_quote.confirm": "投稿を削除",
"confirmations.revoke_quote.title": "投稿を削除しますか?", "confirmations.revoke_quote.title": "投稿を削除しますか?",
"confirmations.unfollow.confirm": "フォロー解除", "confirmations.unfollow.confirm": "フォロー解除",
"confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?",
"confirmations.unfollow.title": "フォローを解除しようとしています",
"content_warning.hide": "内容を隠す", "content_warning.hide": "内容を隠す",
"content_warning.show": "承知して表示", "content_warning.show": "承知して表示",
"content_warning.show_more": "続きを表示", "content_warning.show_more": "続きを表示",

View File

@@ -71,7 +71,6 @@
"confirmations.mute.confirm": "დადუმება", "confirmations.mute.confirm": "დადუმება",
"confirmations.redraft.confirm": "გაუქმება და გადანაწილება", "confirmations.redraft.confirm": "გაუქმება და გადანაწილება",
"confirmations.unfollow.confirm": "ნუღარ მიჰყვები", "confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
"confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
"embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.", "embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.",
"embed.preview": "ესაა თუ როგორც გამოჩნდება:", "embed.preview": "ესაა თუ როგორც გამოჩნდება:",
"emoji_button.activity": "აქტივობა", "emoji_button.activity": "აქტივობა",

View File

@@ -194,7 +194,6 @@
"confirmations.revoke_quote.confirm": "Kkes tasuffeɣt", "confirmations.revoke_quote.confirm": "Kkes tasuffeɣt",
"confirmations.revoke_quote.title": "Kkes tasuffeɣt?", "confirmations.revoke_quote.title": "Kkes tasuffeɣt?",
"confirmations.unfollow.confirm": "Ur ḍḍafaṛ ara", "confirmations.unfollow.confirm": "Ur ḍḍafaṛ ara",
"confirmations.unfollow.message": "Tetḥeqqeḍ belli tebɣiḍ ur teṭafaṛeḍ ara {name}?",
"content_warning.hide": "Ffer tasuffeɣt", "content_warning.hide": "Ffer tasuffeɣt",
"content_warning.show": "Ssken-d akken tebɣu tili", "content_warning.show": "Ssken-d akken tebɣu tili",
"content_warning.show_more": "Sken-d ugar", "content_warning.show_more": "Sken-d ugar",

View File

@@ -140,7 +140,6 @@
"confirmations.mute.confirm": "Үнсіз қылу", "confirmations.mute.confirm": "Үнсіз қылу",
"confirmations.redraft.confirm": "Өшіруді құптау", "confirmations.redraft.confirm": "Өшіруді құптау",
"confirmations.unfollow.confirm": "Оқымау", "confirmations.unfollow.confirm": "Оқымау",
"confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?",
"conversation.delete": "Пікірталасты өшіру", "conversation.delete": "Пікірталасты өшіру",
"conversation.mark_as_read": "Оқылды деп белгіле", "conversation.mark_as_read": "Оқылды деп белгіле",
"conversation.open": "Пікірталасты қарау", "conversation.open": "Пікірталасты қарау",

View File

@@ -252,8 +252,6 @@
"confirmations.revoke_quote.message": "이 작업은 되돌릴 수 없습니다.", "confirmations.revoke_quote.message": "이 작업은 되돌릴 수 없습니다.",
"confirmations.revoke_quote.title": "게시물을 지울까요?", "confirmations.revoke_quote.title": "게시물을 지울까요?",
"confirmations.unfollow.confirm": "팔로우 해제", "confirmations.unfollow.confirm": "팔로우 해제",
"confirmations.unfollow.message": "정말로 {name} 님을 팔로우 해제하시겠습니까?",
"confirmations.unfollow.title": "사용자를 언팔로우 할까요?",
"content_warning.hide": "게시물 숨기기", "content_warning.hide": "게시물 숨기기",
"content_warning.show": "무시하고 보기", "content_warning.show": "무시하고 보기",
"content_warning.show_more": "더 보기", "content_warning.show_more": "더 보기",

View File

@@ -169,7 +169,6 @@
"confirmations.redraft.confirm": "Jê bibe & ji nû ve serrast bike", "confirmations.redraft.confirm": "Jê bibe & ji nû ve serrast bike",
"confirmations.redraft.message": "Bi rastî tu dixwazî şandî ye jê bibî û ji nû ve reşnivîsek çê bikî? Bijarte û şandî wê wenda bibin û bersivên ji bo şandiyê resen wê sêwî bimînin.", "confirmations.redraft.message": "Bi rastî tu dixwazî şandî ye jê bibî û ji nû ve reşnivîsek çê bikî? Bijarte û şandî wê wenda bibin û bersivên ji bo şandiyê resen wê sêwî bimînin.",
"confirmations.unfollow.confirm": "Neşopîne", "confirmations.unfollow.confirm": "Neşopîne",
"confirmations.unfollow.message": "Ma tu dixwazî ku dev ji şopa {name} berdî?",
"content_warning.show_more": "Bêtir nîşan bide", "content_warning.show_more": "Bêtir nîşan bide",
"conversation.delete": "Axaftinê jê bibe", "conversation.delete": "Axaftinê jê bibe",
"conversation.mark_as_read": "Wekî xwendî nîşan bide", "conversation.mark_as_read": "Wekî xwendî nîşan bide",

View File

@@ -86,7 +86,6 @@
"confirmations.mute.confirm": "Tawhe", "confirmations.mute.confirm": "Tawhe",
"confirmations.redraft.confirm": "Dilea & daskynskrifa", "confirmations.redraft.confirm": "Dilea & daskynskrifa",
"confirmations.unfollow.confirm": "Anholya", "confirmations.unfollow.confirm": "Anholya",
"confirmations.unfollow.message": "Owgh hwi sur a vynnes anholya {name}?",
"conversation.delete": "Dilea kesklapp", "conversation.delete": "Dilea kesklapp",
"conversation.mark_as_read": "Merkya vel redys", "conversation.mark_as_read": "Merkya vel redys",
"conversation.open": "Gweles kesklapp", "conversation.open": "Gweles kesklapp",

View File

@@ -214,8 +214,6 @@
"confirmations.revoke_quote.confirm": "Kita puvlikasyon", "confirmations.revoke_quote.confirm": "Kita puvlikasyon",
"confirmations.revoke_quote.title": "Kitar puvlikasyon?", "confirmations.revoke_quote.title": "Kitar puvlikasyon?",
"confirmations.unfollow.confirm": "Desige", "confirmations.unfollow.confirm": "Desige",
"confirmations.unfollow.message": "Estas siguro ke keres deshar de segir a {name}?",
"confirmations.unfollow.title": "Desige utilizador?",
"content_warning.hide": "Eskonde puvlikasyon", "content_warning.hide": "Eskonde puvlikasyon",
"content_warning.show": "Amostra entanto", "content_warning.show": "Amostra entanto",
"content_warning.show_more": "Amostra mas", "content_warning.show_more": "Amostra mas",

View File

@@ -232,8 +232,6 @@
"confirmations.remove_from_followers.message": "{name} nustos jus sekti. Ar tikrai norite tęsti?", "confirmations.remove_from_followers.message": "{name} nustos jus sekti. Ar tikrai norite tęsti?",
"confirmations.remove_from_followers.title": "Šalinti sekėją?", "confirmations.remove_from_followers.title": "Šalinti sekėją?",
"confirmations.unfollow.confirm": "Nebesekti", "confirmations.unfollow.confirm": "Nebesekti",
"confirmations.unfollow.message": "Ar tikrai nori nebesekti {name}?",
"confirmations.unfollow.title": "Nebesekti naudotoją?",
"content_warning.hide": "Slėpti įrašą", "content_warning.hide": "Slėpti įrašą",
"content_warning.show": "Rodyti vis tiek", "content_warning.show": "Rodyti vis tiek",
"content_warning.show_more": "Rodyti daugiau", "content_warning.show_more": "Rodyti daugiau",

View File

@@ -243,8 +243,6 @@
"confirmations.revoke_quote.message": "Šo darbību nevar atsaukt.", "confirmations.revoke_quote.message": "Šo darbību nevar atsaukt.",
"confirmations.revoke_quote.title": "Noņemt ierakstu?", "confirmations.revoke_quote.title": "Noņemt ierakstu?",
"confirmations.unfollow.confirm": "Pārstāt sekot", "confirmations.unfollow.confirm": "Pārstāt sekot",
"confirmations.unfollow.message": "Vai tiešam vairs nevēlies sekot lietotājam {name}?",
"confirmations.unfollow.title": "Pārtraukt sekošanu lietotājam?",
"content_warning.hide": "Paslēpt ierakstu", "content_warning.hide": "Paslēpt ierakstu",
"content_warning.show": "Tomēr rādīt", "content_warning.show": "Tomēr rādīt",
"content_warning.show_more": "Rādīt vairāk", "content_warning.show_more": "Rādīt vairāk",

View File

@@ -82,7 +82,6 @@
"confirmations.logout.message": "Дали сте сигурни дека сакате да се одјавите?", "confirmations.logout.message": "Дали сте сигурни дека сакате да се одјавите?",
"confirmations.mute.confirm": "Заќути", "confirmations.mute.confirm": "Заќути",
"confirmations.unfollow.confirm": "Одследи", "confirmations.unfollow.confirm": "Одследи",
"confirmations.unfollow.message": "Сигурни сте дека ќе го отследите {name}?",
"conversation.delete": "Избриши разговор", "conversation.delete": "Избриши разговор",
"conversation.mark_as_read": "Означете како прочитано", "conversation.mark_as_read": "Означете како прочитано",
"conversation.open": "Прегледај разговор", "conversation.open": "Прегледај разговор",

View File

@@ -139,7 +139,6 @@
"confirmations.mute.confirm": "നിശ്ശബ്ദമാക്കുക", "confirmations.mute.confirm": "നിശ്ശബ്ദമാക്കുക",
"confirmations.redraft.confirm": "മായിച്ച് മാറ്റങ്ങൾ വരുത്തി വീണ്ടും എഴുതുക", "confirmations.redraft.confirm": "മായിച്ച് മാറ്റങ്ങൾ വരുത്തി വീണ്ടും എഴുതുക",
"confirmations.unfollow.confirm": "പിന്തുടരുന്നത് നിര്‍ത്തുക", "confirmations.unfollow.confirm": "പിന്തുടരുന്നത് നിര്‍ത്തുക",
"confirmations.unfollow.message": "നിങ്ങൾ {name} യെ പിന്തുടരുന്നത് നിർത്തുവാൻ തീർച്ചയായും തീരുമാനിച്ചുവോ?",
"conversation.delete": "സംഭാഷണം മായിക്കുക", "conversation.delete": "സംഭാഷണം മായിക്കുക",
"conversation.mark_as_read": "വായിച്ചതായി അടയാളപ്പെടുത്തുക", "conversation.mark_as_read": "വായിച്ചതായി അടയാളപ്പെടുത്തുക",
"conversation.open": "സംഭാഷണം കാണുക", "conversation.open": "സംഭാഷണം കാണുക",

View File

@@ -222,8 +222,6 @@
"confirmations.redraft.message": "Adakah anda pasti anda ingin memadam hantaran ini dan gubal semula? Sukaan dan galakan akan hilang, dan balasan ke hantaran asal akan menjadi yatim.", "confirmations.redraft.message": "Adakah anda pasti anda ingin memadam hantaran ini dan gubal semula? Sukaan dan galakan akan hilang, dan balasan ke hantaran asal akan menjadi yatim.",
"confirmations.redraft.title": "Padam & gubah semula hantaran?", "confirmations.redraft.title": "Padam & gubah semula hantaran?",
"confirmations.unfollow.confirm": "Nyahikut", "confirmations.unfollow.confirm": "Nyahikut",
"confirmations.unfollow.message": "Adakah anda pasti anda ingin nyahikuti {name}?",
"confirmations.unfollow.title": "Berhenti mengikut pengguna?",
"content_warning.hide": "Sorok hantaran", "content_warning.hide": "Sorok hantaran",
"content_warning.show": "Tunjuk saja", "content_warning.show": "Tunjuk saja",
"content_warning.show_more": "Tunjuk lebih", "content_warning.show_more": "Tunjuk lebih",

View File

@@ -152,7 +152,6 @@
"confirmations.redraft.confirm": "ဖျက်ပြီး ပြန်လည်ရေးမည်။", "confirmations.redraft.confirm": "ဖျက်ပြီး ပြန်လည်ရေးမည်။",
"confirmations.redraft.message": "သင် ဒီပိုစ့်ကိုဖျက်ပြီး ပြန်တည်းဖြတ်မှာ သေချာပြီလား။ ကြယ်ပွင့်​တွေ နဲ့ ပြန်မျှ​ဝေမှု​တွေကိုဆုံးရှုံးမည်။မူရင်းပို့စ်ဆီကို ပြန်စာ​တွေမှာလည်း​ \nပိုစ့်ကိုတွေ့ရမည်မဟုတ်တော့ပါ။.", "confirmations.redraft.message": "သင် ဒီပိုစ့်ကိုဖျက်ပြီး ပြန်တည်းဖြတ်မှာ သေချာပြီလား။ ကြယ်ပွင့်​တွေ နဲ့ ပြန်မျှ​ဝေမှု​တွေကိုဆုံးရှုံးမည်။မူရင်းပို့စ်ဆီကို ပြန်စာ​တွေမှာလည်း​ \nပိုစ့်ကိုတွေ့ရမည်မဟုတ်တော့ပါ။.",
"confirmations.unfollow.confirm": "စောင့်ကြည့်ခြင်းအား ပယ်ဖျက်မည်", "confirmations.unfollow.confirm": "စောင့်ကြည့်ခြင်းအား ပယ်ဖျက်မည်",
"confirmations.unfollow.message": "{name} ကို စောင်ကြည့်ခြင်းအား ပယ်ဖျက်မည်မှာသေချာပါသလား။",
"conversation.delete": "ဤစကားပြောဆိုမှုကို ဖျက်ပစ်မည်", "conversation.delete": "ဤစကားပြောဆိုမှုကို ဖျက်ပစ်မည်",
"conversation.mark_as_read": "ဖတ်ပြီးသားအဖြစ်မှတ်ထားပါ", "conversation.mark_as_read": "ဖတ်ပြီးသားအဖြစ်မှတ်ထားပါ",
"conversation.open": "Conversation ကိုကြည့်မည်", "conversation.open": "Conversation ကိုကြည့်မည်",

View File

@@ -258,8 +258,6 @@
"confirmations.revoke_quote.message": "Tsit ê動作bē當復原。", "confirmations.revoke_quote.message": "Tsit ê動作bē當復原。",
"confirmations.revoke_quote.title": "Kám beh thâi掉PO文", "confirmations.revoke_quote.title": "Kám beh thâi掉PO文",
"confirmations.unfollow.confirm": "取消跟tuè", "confirmations.unfollow.confirm": "取消跟tuè",
"confirmations.unfollow.message": "Lí kám確定無愛跟tuè {name}",
"confirmations.unfollow.title": "Kám beh取消跟tuè tsit ê用者?",
"content_warning.hide": "Am-khàm PO文", "content_warning.hide": "Am-khàm PO文",
"content_warning.show": "Mā tio̍h顯示", "content_warning.show": "Mā tio̍h顯示",
"content_warning.show_more": "其他內容", "content_warning.show_more": "其他內容",

View File

@@ -143,8 +143,6 @@
"confirmations.redraft.confirm": "मेटाएर पुन: ड्राफ्ट गर्नुहोस्", "confirmations.redraft.confirm": "मेटाएर पुन: ड्राफ्ट गर्नुहोस्",
"confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?", "confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?",
"confirmations.unfollow.confirm": "अनफलो गर्नुहोस्", "confirmations.unfollow.confirm": "अनफलो गर्नुहोस्",
"confirmations.unfollow.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ {name}लाई अनफलो गर्न चाहनुहुन्छ?",
"confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने?",
"disabled_account_banner.account_settings": "खाता सेटिङहरू", "disabled_account_banner.account_settings": "खाता सेटिङहरू",
"empty_column.direct": "तपाईंले अहिलेसम्म कुनै पनि प्राइवेट उल्लेखहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।", "empty_column.direct": "तपाईंले अहिलेसम्म कुनै पनि प्राइवेट उल्लेखहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",
"empty_column.follow_requests": "तपाईंले अहिलेसम्म कुनै पनि फलो अनुरोधहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।", "empty_column.follow_requests": "तपाईंले अहिलेसम्म कुनै पनि फलो अनुरोधहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",

View File

@@ -42,7 +42,7 @@
"account.follow": "Volgen", "account.follow": "Volgen",
"account.follow_back": "Terugvolgen", "account.follow_back": "Terugvolgen",
"account.follow_back_short": "Terugvolgen", "account.follow_back_short": "Terugvolgen",
"account.follow_request": "Verzoeken om te volgen", "account.follow_request": "Volgverzoek",
"account.follow_request_cancel": "Verzoek annuleren", "account.follow_request_cancel": "Verzoek annuleren",
"account.follow_request_cancel_short": "Annuleren", "account.follow_request_cancel_short": "Annuleren",
"account.follow_request_short": "Verzoek", "account.follow_request_short": "Verzoek",
@@ -257,9 +257,12 @@
"confirmations.revoke_quote.confirm": "Bericht verwijderen", "confirmations.revoke_quote.confirm": "Bericht verwijderen",
"confirmations.revoke_quote.message": "Deze actie kan niet ongedaan worden gemaakt.", "confirmations.revoke_quote.message": "Deze actie kan niet ongedaan worden gemaakt.",
"confirmations.revoke_quote.title": "Bericht verwijderen?", "confirmations.revoke_quote.title": "Bericht verwijderen?",
"confirmations.unblock.confirm": "Deblokkeren",
"confirmations.unblock.title": "{name} deblokkeren?",
"confirmations.unfollow.confirm": "Ontvolgen", "confirmations.unfollow.confirm": "Ontvolgen",
"confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?", "confirmations.unfollow.title": "{name} ontvolgen?",
"confirmations.unfollow.title": "Gebruiker ontvolgen?", "confirmations.withdraw_request.confirm": "Verzoek intrekken",
"confirmations.withdraw_request.title": "Verzoek intrekken om {name} te volgen?",
"content_warning.hide": "Bericht verbergen", "content_warning.hide": "Bericht verbergen",
"content_warning.show": "Alsnog tonen", "content_warning.show": "Alsnog tonen",
"content_warning.show_more": "Meer tonen", "content_warning.show_more": "Meer tonen",
@@ -920,6 +923,8 @@
"status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk", "status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk",
"status.quotes": "{count, plural, one {citaat} other {citaten}}", "status.quotes": "{count, plural, one {citaat} other {citaten}}",
"status.quotes.empty": "Niemand heeft dit bericht nog geciteerd. Wanneer iemand dat doet, wordt dat hier getoond.", "status.quotes.empty": "Niemand heeft dit bericht nog geciteerd. Wanneer iemand dat doet, wordt dat hier getoond.",
"status.quotes.local_other_disclaimer": "Citaten afgewezen door de auteur worden niet getoond.",
"status.quotes.remote_other_disclaimer": "Alleen citaten van {domain} worden hier gegarandeerd weergegeven. Citaten afgewezen door de auteur worden niet getoond.",
"status.read_more": "Meer lezen", "status.read_more": "Meer lezen",
"status.reblog": "Boosten", "status.reblog": "Boosten",
"status.reblog_or_quote": "Boosten of citeren", "status.reblog_or_quote": "Boosten of citeren",

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