mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-23 15:29:07 +00:00
Merge commit 'f69ca085dbfca2253404574dcdc4dc6c2aaa35c0' into glitch-soc/merge-upstream
This commit is contained in:
@@ -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! :write, :'write:statuses' }, only: :revoke
|
||||
|
||||
before_action :check_owner!
|
||||
before_action :set_statuses, only: :index
|
||||
|
||||
before_action :set_quote, only: :revoke
|
||||
after_action :insert_pagination_headers, only: :index
|
||||
|
||||
def index
|
||||
cache_if_unauthenticated!
|
||||
@statuses = load_statuses
|
||||
render json: @statuses, each_serializer: REST::StatusSerializer
|
||||
end
|
||||
|
||||
@@ -24,18 +24,26 @@ class Api::V1::Statuses::QuotesController < Api::V1::Statuses::BaseController
|
||||
|
||||
private
|
||||
|
||||
def check_owner!
|
||||
authorize @status, :list_quotes?
|
||||
end
|
||||
|
||||
def set_quote
|
||||
@quote = @status.quotes.find_by!(status_id: params[:id])
|
||||
end
|
||||
|
||||
def load_statuses
|
||||
def set_statuses
|
||||
scope = default_statuses
|
||||
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
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def pagination_max_id
|
||||
@statuses.last.quote.id
|
||||
end
|
||||
|
||||
def pagination_since_id
|
||||
@statuses.first.quote.id
|
||||
end
|
||||
attr_reader :pagination_max_id, :pagination_since_id
|
||||
|
||||
def records_continue?
|
||||
@statuses.size == limit_param(DEFAULT_STATUSES_LIMIT)
|
||||
@records_continue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { useLinks } from 'mastodon/hooks/useLinks';
|
||||
|
||||
import { EmojiHTML } from '../features/emoji/emoji_html';
|
||||
import { useAppSelector } from '../store';
|
||||
import { isModernEmojiEnabled } from '../utils/environment';
|
||||
|
||||
import { AnimateEmojiProvider } from './emoji/context';
|
||||
import { EmojiHTML } from './emoji/html';
|
||||
|
||||
interface AccountBioProps {
|
||||
className: string;
|
||||
accountId: string;
|
||||
@@ -44,13 +48,13 @@ export const AccountBio: React.FC<AccountBioProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${className} translate`}
|
||||
<AnimateEmojiProvider
|
||||
className={classNames(className, 'translate')}
|
||||
onClickCapture={handleClick}
|
||||
ref={handleNodeChange}
|
||||
>
|
||||
<EmojiHTML htmlString={note} extraEmojis={extraEmojis} />
|
||||
</div>
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
|
||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
import { AnimateEmojiProvider } from '../emoji/context';
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
import { Skeleton } from '../skeleton';
|
||||
|
||||
import type { DisplayNameProps } from './index';
|
||||
@@ -14,9 +15,10 @@ export const DisplayNameWithoutDomain: FC<
|
||||
ComponentPropsWithoutRef<'span'>
|
||||
> = ({ account, className, children, ...props }) => {
|
||||
return (
|
||||
<span
|
||||
<AnimateEmojiProvider
|
||||
{...props}
|
||||
className={classNames('display-name animate-parent', className)}
|
||||
as='span'
|
||||
className={classNames('display-name', className)}
|
||||
>
|
||||
<bdi>
|
||||
{account ? (
|
||||
@@ -27,8 +29,8 @@ export const DisplayNameWithoutDomain: FC<
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html')
|
||||
}
|
||||
shallow
|
||||
as='strong'
|
||||
extraEmojis={account.get('emojis')}
|
||||
/>
|
||||
) : (
|
||||
<strong className='display-name__html'>
|
||||
@@ -37,6 +39,6 @@ export const DisplayNameWithoutDomain: FC<
|
||||
)}
|
||||
</bdi>
|
||||
{children}
|
||||
</span>
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { ComponentPropsWithoutRef, FC } from 'react';
|
||||
|
||||
import { EmojiHTML } from '@/mastodon/features/emoji/emoji_html';
|
||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||
|
||||
import { EmojiHTML } from '../emoji/html';
|
||||
|
||||
import type { DisplayNameProps } from './index';
|
||||
|
||||
export const DisplayNameSimple: FC<
|
||||
@@ -12,12 +13,19 @@ export const DisplayNameSimple: FC<
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
const accountName = isModernEmojiEnabled()
|
||||
? account.get('display_name')
|
||||
: account.get('display_name_html');
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
108
app/javascript/mastodon/components/emoji/context.tsx
Normal file
108
app/javascript/mastodon/components/emoji/context.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
61
app/javascript/mastodon/components/emoji/html.tsx
Normal file
61
app/javascript/mastodon/components/emoji/html.tsx
Normal 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;
|
||||
99
app/javascript/mastodon/components/emoji/index.tsx
Normal file
99
app/javascript/mastodon/components/emoji/index.tsx
Normal 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}`} />;
|
||||
});
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { useIdentity } from '@/mastodon/identity_context';
|
||||
import {
|
||||
fetchRelationships,
|
||||
followAccount,
|
||||
unblockAccount,
|
||||
unmuteAccount,
|
||||
} from 'mastodon/actions/accounts';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
@@ -59,7 +58,8 @@ export const FollowButton: React.FC<{
|
||||
accountId?: string;
|
||||
compact?: boolean;
|
||||
labelLength?: 'auto' | 'short' | 'long';
|
||||
}> = ({ accountId, compact, labelLength = 'auto' }) => {
|
||||
className?: string;
|
||||
}> = ({ accountId, compact, labelLength = 'auto', className }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { signedIn } = useIdentity();
|
||||
@@ -96,12 +96,24 @@ export const FollowButton: React.FC<{
|
||||
return;
|
||||
} else if (relationship.muting) {
|
||||
dispatch(unmuteAccount(accountId));
|
||||
} else if (account && (relationship.following || relationship.requested)) {
|
||||
} else if (account && relationship.following) {
|
||||
dispatch(
|
||||
openModal({ modalType: 'CONFIRM_UNFOLLOW', modalProps: { account } }),
|
||||
);
|
||||
} else if (account && relationship.requested) {
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_WITHDRAW_REQUEST',
|
||||
modalProps: { account },
|
||||
}),
|
||||
);
|
||||
} else if (relationship.blocking) {
|
||||
dispatch(unblockAccount(accountId));
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'CONFIRM_UNBLOCK',
|
||||
modalProps: { account },
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
dispatch(followAccount(accountId));
|
||||
}
|
||||
@@ -144,7 +156,7 @@ export const FollowButton: React.FC<{
|
||||
href='/settings/profile'
|
||||
target='_blank'
|
||||
rel='noopener'
|
||||
className={classNames('button button-secondary', {
|
||||
className={classNames(className, 'button button-secondary', {
|
||||
'button--compact': compact,
|
||||
})}
|
||||
>
|
||||
@@ -158,13 +170,12 @@ export const FollowButton: React.FC<{
|
||||
onClick={handleClick}
|
||||
disabled={
|
||||
relationship?.blocked_by ||
|
||||
relationship?.blocking ||
|
||||
(!(relationship?.following || relationship?.requested) &&
|
||||
(account?.suspended || !!account?.moved))
|
||||
}
|
||||
secondary={following}
|
||||
compact={compact}
|
||||
className={following ? 'button--destructive' : undefined}
|
||||
className={classNames(className, { 'button--destructive': following })}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
|
||||
@@ -13,10 +13,12 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { Poll } from 'mastodon/components/poll';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
import { EmojiHTML } from '../features/emoji/emoji_html';
|
||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
|
||||
import { isModernEmojiEnabled } from '../utils/environment';
|
||||
|
||||
import { EmojiHTML } from './emoji/html';
|
||||
|
||||
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import { fetchServer } from 'mastodon/actions/server';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
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 { store } from 'mastodon/store';
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import UI from 'mastodon/features/ui';
|
||||
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 { store } from 'mastodon/store';
|
||||
import { isProduction } from 'mastodon/utils/environment';
|
||||
|
||||
@@ -8,6 +8,7 @@ import { NavLink } from 'react-router-dom';
|
||||
|
||||
import { AccountBio } from '@/mastodon/components/account_bio';
|
||||
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 LockIcon from '@/material-icons/400-24px/lock.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 { Avatar } from 'mastodon/components/avatar';
|
||||
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CopyIconButton } from 'mastodon/components/copy_icon_button';
|
||||
import {
|
||||
FollowersCounter,
|
||||
@@ -383,7 +383,7 @@ export const AccountHeader: React.FC<{
|
||||
const isRemote = account?.acct !== account?.username;
|
||||
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
|
||||
|
||||
const menu = useMemo(() => {
|
||||
const menuItems = useMemo(() => {
|
||||
const arr: MenuItem[] = [];
|
||||
|
||||
if (!account) {
|
||||
@@ -605,6 +605,15 @@ export const AccountHeader: React.FC<{
|
||||
handleUnblockDomain,
|
||||
]);
|
||||
|
||||
const menu = accountId !== me && (
|
||||
<Dropdown
|
||||
disabled={menuItems.length === 0}
|
||||
items={menuItems}
|
||||
icon='ellipsis-v'
|
||||
iconComponent={MoreHorizIcon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
@@ -718,21 +727,16 @@ export const AccountHeader: React.FC<{
|
||||
);
|
||||
}
|
||||
|
||||
if (relationship?.blocking) {
|
||||
const isMovedAndUnfollowedAccount = account.moved && !relationship?.following;
|
||||
|
||||
if (!isMovedAndUnfollowedAccount) {
|
||||
actionBtn = (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.unblock, {
|
||||
name: account.username,
|
||||
})}
|
||||
onClick={handleBlock}
|
||||
<FollowButton
|
||||
accountId={accountId}
|
||||
className='account__header__follow-button'
|
||||
labelLength='long'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
actionBtn = <FollowButton accountId={accountId} />;
|
||||
}
|
||||
|
||||
if (account.moved && !relationship?.following) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (account.locked) {
|
||||
@@ -777,8 +781,8 @@ export const AccountHeader: React.FC<{
|
||||
<MovedNote accountId={account.id} targetAccountId={account.moved} />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames('account__header animate-parent', {
|
||||
<AnimateEmojiProvider
|
||||
className={classNames('account__header', {
|
||||
inactive: !!account.moved,
|
||||
})}
|
||||
>
|
||||
@@ -814,18 +818,11 @@ export const AccountHeader: React.FC<{
|
||||
/>
|
||||
</a>
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
<div className='account__header__buttons account__header__buttons--desktop'>
|
||||
{!hidden && actionBtn}
|
||||
{!hidden && bellBtn}
|
||||
{!hidden && shareBtn}
|
||||
{accountId !== me && (
|
||||
<Dropdown
|
||||
disabled={menu.length === 0}
|
||||
items={menu}
|
||||
icon='ellipsis-v'
|
||||
iconComponent={MoreHorizIcon}
|
||||
/>
|
||||
)}
|
||||
{!hidden && actionBtn}
|
||||
{menu}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -855,6 +852,12 @@ export const AccountHeader: React.FC<{
|
||||
<FamiliarFollowers accountId={accountId} />
|
||||
)}
|
||||
|
||||
<div className='account__header__buttons account__header__buttons--mobile'>
|
||||
{!hidden && actionBtn}
|
||||
{!hidden && bellBtn}
|
||||
{menu}
|
||||
</div>
|
||||
|
||||
{!(suspended || hidden) && (
|
||||
<div className='account__header__extra'>
|
||||
<div
|
||||
@@ -967,7 +970,7 @@ export const AccountHeader: React.FC<{
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AnimateEmojiProvider>
|
||||
|
||||
{!(hideTabs || hidden) && (
|
||||
<div className='account__section-headline'>
|
||||
|
||||
@@ -25,6 +25,7 @@ import StatusContent from 'mastodon/components/status_content';
|
||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
|
||||
|
||||
const messages = defineMessages({
|
||||
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')} />
|
||||
</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> }} />
|
||||
</div>
|
||||
</AnimateEmojiProvider>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
|
||||
@@ -23,8 +23,6 @@ export const EMOJI_MODE_TWEMOJI = 'twemoji';
|
||||
export const EMOJI_TYPE_UNICODE = 'unicode';
|
||||
export const EMOJI_TYPE_CUSTOM = 'custom';
|
||||
|
||||
export const EMOJI_STATE_MISSING = 'missing';
|
||||
|
||||
export const EMOJIS_WITH_DARK_BORDER = [
|
||||
'🎱', // 1F3B1
|
||||
'🐜', // 1F41C
|
||||
|
||||
@@ -197,11 +197,18 @@ function toLoadedLocale(localeString: string) {
|
||||
log(`Locale ${locale} is different from provided ${localeString}`);
|
||||
}
|
||||
if (!loadedLocales.has(locale)) {
|
||||
throw new Error(`Locale ${locale} is not loaded in emoji database`);
|
||||
throw new LocaleNotLoadedError(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> {
|
||||
if (loadedLocales.has(locale)) {
|
||||
return true;
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import initialState from '@/mastodon/initial_state';
|
||||
import { initialState } from '@/mastodon/initial_state';
|
||||
import { loadWorker } from '@/mastodon/utils/workers';
|
||||
|
||||
import { toSupportedLocale } from './locale';
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { flattenEmojiData } from 'emojibase';
|
||||
import type { CompactEmoji, FlatCompactEmoji } from 'emojibase';
|
||||
|
||||
import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji';
|
||||
|
||||
import {
|
||||
putEmojiData,
|
||||
putCustomEmojiData,
|
||||
@@ -10,7 +8,7 @@ import {
|
||||
putLatestEtag,
|
||||
} from './database';
|
||||
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
|
||||
import type { LocaleOrCustom } from './types';
|
||||
import type { CustomEmojiData, LocaleOrCustom } from './types';
|
||||
import { emojiLogger } from './utils';
|
||||
|
||||
const log = emojiLogger('loader');
|
||||
@@ -27,7 +25,7 @@ export async function importEmojiData(localeString: string) {
|
||||
}
|
||||
|
||||
export async function importCustomEmojiData() {
|
||||
const emojis = await fetchAndCheckEtag<ApiCustomEmojiJSON[]>('custom');
|
||||
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
|
||||
if (!emojis) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,8 @@ import { flattenEmojiData } from 'emojibase';
|
||||
import unicodeRawEmojis from 'emojibase-data/en/data.json';
|
||||
|
||||
import {
|
||||
twemojiHasBorder,
|
||||
twemojiToUnicodeInfo,
|
||||
unicodeToTwemojiHex,
|
||||
CODES_WITH_DARK_BORDER,
|
||||
CODES_WITH_LIGHT_BORDER,
|
||||
emojiToUnicodeHex,
|
||||
} 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', () => {
|
||||
const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode));
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { isList } from 'immutable';
|
||||
|
||||
import { assetHost } from '@/mastodon/utils/config';
|
||||
|
||||
import {
|
||||
VARIATION_SELECTOR_CODE,
|
||||
KEYCAP_CODE,
|
||||
@@ -9,11 +11,7 @@ import {
|
||||
EMOJIS_WITH_DARK_BORDER,
|
||||
EMOJIS_WITH_LIGHT_BORDER,
|
||||
} from './constants';
|
||||
import type {
|
||||
CustomEmojiMapArg,
|
||||
ExtraCustomEmojiMap,
|
||||
TwemojiBorderInfo,
|
||||
} from './types';
|
||||
import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types';
|
||||
|
||||
// Misc codes that have special handling
|
||||
const SKIER_CODE = 0x26f7;
|
||||
@@ -67,21 +65,17 @@ export const CODES_WITH_DARK_BORDER =
|
||||
export const CODES_WITH_LIGHT_BORDER =
|
||||
EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex);
|
||||
|
||||
export function twemojiHasBorder(twemojiHex: string): TwemojiBorderInfo {
|
||||
const normalizedHex = twemojiHex.toUpperCase();
|
||||
let hasLightBorder = false;
|
||||
let hasDarkBorder = false;
|
||||
if (CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) {
|
||||
hasLightBorder = true;
|
||||
export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string {
|
||||
const normalizedHex = unicodeToTwemojiHex(unicodeHex);
|
||||
let url = `${assetHost}/emoji/${normalizedHex}`;
|
||||
if (darkMode && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) {
|
||||
url += '_border';
|
||||
}
|
||||
if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) {
|
||||
hasDarkBorder = true;
|
||||
url += '_border';
|
||||
}
|
||||
return {
|
||||
hexCode: twemojiHex,
|
||||
hasLightBorder,
|
||||
hasDarkBorder,
|
||||
};
|
||||
url += '.svg';
|
||||
return url;
|
||||
}
|
||||
|
||||
interface TwemojiSpecificEmoji {
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { customEmojiFactory, unicodeEmojiFactory } from '@/testing/factories';
|
||||
|
||||
import {
|
||||
EMOJI_MODE_NATIVE,
|
||||
EMOJI_MODE_NATIVE_WITH_FLAGS,
|
||||
EMOJI_MODE_TWEMOJI,
|
||||
} from './constants';
|
||||
import { EMOJI_MODE_TWEMOJI } from './constants';
|
||||
import * as db from './database';
|
||||
import {
|
||||
emojifyElement,
|
||||
@@ -12,7 +8,7 @@ import {
|
||||
testCacheClear,
|
||||
tokenizeText,
|
||||
} from './render';
|
||||
import type { EmojiAppState, ExtraCustomEmojiMap } from './types';
|
||||
import type { EmojiAppState } from './types';
|
||||
|
||||
function mockDatabase() {
|
||||
return {
|
||||
@@ -40,18 +36,6 @@ const expectedSmileImage =
|
||||
'<img draggable="false" class="emojione" alt="😊" title="smiling face with smiling eyes" src="/emoji/1f60a.svg">';
|
||||
const expectedFlagImage =
|
||||
'<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> = {}) {
|
||||
return {
|
||||
@@ -86,64 +70,10 @@ describe('emojifyElement', () => {
|
||||
'en',
|
||||
);
|
||||
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 () => {
|
||||
mockDatabase();
|
||||
const actual = await emojifyElement(
|
||||
@@ -165,28 +95,9 @@ describe('emojifyText', () => {
|
||||
const actual = await emojifyText('Hello 😊🇪🇺!', testAppState());
|
||||
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', () => {
|
||||
test('returns empty array for string with only whitespace', () => {
|
||||
expect(tokenizeText(' \n')).toEqual([]);
|
||||
});
|
||||
|
||||
test('returns an array of text to be a single token', () => {
|
||||
expect(tokenizeText('Hello')).toEqual(['Hello']);
|
||||
});
|
||||
@@ -212,7 +123,7 @@ describe('tokenizeText', () => {
|
||||
'Hello ',
|
||||
{
|
||||
type: 'custom',
|
||||
code: 'smile',
|
||||
code: ':smile:',
|
||||
},
|
||||
'!!',
|
||||
]);
|
||||
@@ -223,7 +134,7 @@ describe('tokenizeText', () => {
|
||||
'Hello ',
|
||||
{
|
||||
type: 'custom',
|
||||
code: 'smile_123',
|
||||
code: ':smile_123:',
|
||||
},
|
||||
'!!',
|
||||
]);
|
||||
@@ -239,7 +150,7 @@ describe('tokenizeText', () => {
|
||||
' ',
|
||||
{
|
||||
type: 'custom',
|
||||
code: 'smile',
|
||||
code: ':smile:',
|
||||
},
|
||||
'!!',
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||
import { createLimitedCache } from '@/mastodon/utils/cache';
|
||||
import { assetHost } from '@/mastodon/utils/config';
|
||||
import * as perf from '@/mastodon/utils/performance';
|
||||
|
||||
import {
|
||||
@@ -8,38 +7,130 @@ import {
|
||||
EMOJI_MODE_NATIVE_WITH_FLAGS,
|
||||
EMOJI_TYPE_UNICODE,
|
||||
EMOJI_TYPE_CUSTOM,
|
||||
EMOJI_STATE_MISSING,
|
||||
} from './constants';
|
||||
import {
|
||||
loadCustomEmojiByShortcode,
|
||||
loadEmojiByHexcode,
|
||||
LocaleNotLoadedError,
|
||||
searchCustomEmojisByShortcodes,
|
||||
searchEmojisByHexcodes,
|
||||
} from './database';
|
||||
import {
|
||||
emojiToUnicodeHex,
|
||||
twemojiHasBorder,
|
||||
unicodeToTwemojiHex,
|
||||
} from './normalize';
|
||||
import { importEmojiData } from './loader';
|
||||
import { emojiToUnicodeHex, unicodeHexToUrl } from './normalize';
|
||||
import type {
|
||||
CustomEmojiToken,
|
||||
EmojiAppState,
|
||||
EmojiLoadedState,
|
||||
EmojiMode,
|
||||
EmojiState,
|
||||
EmojiStateCustom,
|
||||
EmojiStateMap,
|
||||
EmojiToken,
|
||||
EmojiStateUnicode,
|
||||
ExtraCustomEmojiMap,
|
||||
LocaleOrCustom,
|
||||
UnicodeEmojiToken,
|
||||
} from './types';
|
||||
import {
|
||||
anyEmojiRegex,
|
||||
emojiLogger,
|
||||
isCustomEmoji,
|
||||
isUnicodeEmoji,
|
||||
stringHasAnyEmoji,
|
||||
stringHasUnicodeFlags,
|
||||
} from './utils';
|
||||
|
||||
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.
|
||||
*/
|
||||
@@ -177,7 +268,11 @@ async function textToElementArray(
|
||||
if (token.type === EMOJI_TYPE_CUSTOM) {
|
||||
const extraEmojiData = extraEmojis[token.code];
|
||||
if (extraEmojiData) {
|
||||
state = { type: EMOJI_TYPE_CUSTOM, data: extraEmojiData };
|
||||
state = {
|
||||
type: EMOJI_TYPE_CUSTOM,
|
||||
data: extraEmojiData,
|
||||
code: token.code,
|
||||
};
|
||||
} else {
|
||||
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 (state && typeof state !== 'string') {
|
||||
if (state && typeof state !== 'string' && isStateLoaded(state)) {
|
||||
const image = stateToImage(state, appState);
|
||||
renderedFragments.push(image);
|
||||
continue;
|
||||
@@ -202,11 +297,11 @@ async function textToElementArray(
|
||||
return renderedFragments;
|
||||
}
|
||||
|
||||
type TokenizedText = (string | EmojiToken)[];
|
||||
type TokenizedText = (string | EmojiState)[];
|
||||
|
||||
export function tokenizeText(text: string): TokenizedText {
|
||||
if (!text.trim()) {
|
||||
return [];
|
||||
return [text];
|
||||
}
|
||||
|
||||
const tokens = [];
|
||||
@@ -222,14 +317,14 @@ export function tokenizeText(text: string): TokenizedText {
|
||||
// Custom emoji
|
||||
tokens.push({
|
||||
type: EMOJI_TYPE_CUSTOM,
|
||||
code: code.slice(1, -1), // Remove the colons
|
||||
} satisfies CustomEmojiToken);
|
||||
code,
|
||||
} satisfies EmojiStateCustom);
|
||||
} else {
|
||||
// Unicode emoji
|
||||
tokens.push({
|
||||
type: EMOJI_TYPE_UNICODE,
|
||||
code: code,
|
||||
} satisfies UnicodeEmojiToken);
|
||||
} satisfies EmojiStateUnicode);
|
||||
}
|
||||
lastIndex = match.index + code.length;
|
||||
}
|
||||
@@ -304,13 +399,11 @@ async function loadMissingEmojiIntoCache(
|
||||
const emojis = await searchEmojisByHexcodes(missingEmojis, currentLocale);
|
||||
const cache = cacheForLocale(currentLocale);
|
||||
for (const emoji of emojis) {
|
||||
cache.set(emoji.hexcode, { type: EMOJI_TYPE_UNICODE, data: emoji });
|
||||
}
|
||||
const notFoundEmojis = missingEmojis.filter((code) =>
|
||||
emojis.every((emoji) => emoji.hexcode !== code),
|
||||
);
|
||||
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.
|
||||
cache.set(emoji.hexcode, {
|
||||
type: EMOJI_TYPE_UNICODE,
|
||||
data: emoji,
|
||||
code: emoji.hexcode,
|
||||
});
|
||||
}
|
||||
localeCacheMap.set(currentLocale, cache);
|
||||
}
|
||||
@@ -320,19 +413,17 @@ async function loadMissingEmojiIntoCache(
|
||||
const emojis = await searchCustomEmojisByShortcodes(missingEmojis);
|
||||
const cache = cacheForLocale(EMOJI_TYPE_CUSTOM);
|
||||
for (const emoji of emojis) {
|
||||
cache.set(emoji.shortcode, { type: EMOJI_TYPE_CUSTOM, data: emoji });
|
||||
}
|
||||
const notFoundEmojis = missingEmojis.filter((code) =>
|
||||
emojis.every((emoji) => emoji.shortcode !== code),
|
||||
);
|
||||
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.
|
||||
cache.set(emoji.shortcode, {
|
||||
type: EMOJI_TYPE_CUSTOM,
|
||||
data: emoji,
|
||||
code: emoji.shortcode,
|
||||
});
|
||||
}
|
||||
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 the mode is native or native with flags for non-flag emoji
|
||||
// we can just append the text node directly.
|
||||
@@ -354,18 +445,9 @@ function stateToImage(state: EmojiLoadedState, appState: EmojiAppState) {
|
||||
image.classList.add('emojione');
|
||||
|
||||
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.title = state.data.label;
|
||||
image.src = `${assetHost}/emoji/${fileName}.svg`;
|
||||
image.src = unicodeHexToUrl(state.data.hexcode, appState.darkTheme);
|
||||
} else {
|
||||
// Custom emoji
|
||||
const shortCode = `:${state.data.shortcode}:`;
|
||||
|
||||
@@ -10,7 +10,6 @@ import type {
|
||||
EMOJI_MODE_NATIVE,
|
||||
EMOJI_MODE_NATIVE_WITH_FLAGS,
|
||||
EMOJI_MODE_TWEMOJI,
|
||||
EMOJI_STATE_MISSING,
|
||||
EMOJI_TYPE_CUSTOM,
|
||||
EMOJI_TYPE_UNICODE,
|
||||
} from './constants';
|
||||
@@ -29,45 +28,40 @@ export interface EmojiAppState {
|
||||
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 UnicodeEmojiData = FlatCompactEmoji;
|
||||
export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData;
|
||||
|
||||
export type EmojiStateMissing = typeof EMOJI_STATE_MISSING;
|
||||
type CustomEmojiRenderFields = Pick<
|
||||
CustomEmojiData,
|
||||
'shortcode' | 'static_url' | 'url'
|
||||
>;
|
||||
|
||||
export interface EmojiStateUnicode {
|
||||
type: typeof EMOJI_TYPE_UNICODE;
|
||||
data: UnicodeEmojiData;
|
||||
code: string;
|
||||
data?: UnicodeEmojiData;
|
||||
}
|
||||
export interface EmojiStateCustom {
|
||||
type: typeof EMOJI_TYPE_CUSTOM;
|
||||
data: CustomEmojiRenderFields;
|
||||
code: string;
|
||||
data?: CustomEmojiRenderFields;
|
||||
}
|
||||
export type EmojiState =
|
||||
| EmojiStateMissing
|
||||
| EmojiStateUnicode
|
||||
| EmojiStateCustom;
|
||||
export type EmojiLoadedState = EmojiStateUnicode | EmojiStateCustom;
|
||||
export type EmojiState = EmojiStateUnicode | EmojiStateCustom;
|
||||
export type EmojiLoadedState =
|
||||
| Required<EmojiStateUnicode>
|
||||
| Required<EmojiStateCustom>;
|
||||
|
||||
export type EmojiStateMap = LimitedCache<string, EmojiState>;
|
||||
|
||||
export type CustomEmojiMapArg =
|
||||
| ExtraCustomEmojiMap
|
||||
| ImmutableList<CustomEmoji>;
|
||||
export type CustomEmojiRenderFields = Pick<
|
||||
CustomEmojiData,
|
||||
'shortcode' | 'static_url' | 'url'
|
||||
|
||||
export type ExtraCustomEmojiMap = Record<
|
||||
string,
|
||||
Pick<CustomEmojiData, 'shortcode' | 'static_url' | 'url'>
|
||||
>;
|
||||
export type ExtraCustomEmojiMap = Record<string, CustomEmojiRenderFields>;
|
||||
|
||||
export interface TwemojiBorderInfo {
|
||||
hexCode: string;
|
||||
|
||||
@@ -10,6 +10,13 @@ export function stringHasUnicodeEmoji(input: string): boolean {
|
||||
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 {
|
||||
if (supportsRegExpSets()) {
|
||||
return new RegExp(
|
||||
@@ -27,6 +34,11 @@ export function stringHasUnicodeFlags(input: string): boolean {
|
||||
|
||||
// Constant as this is supported by all browsers.
|
||||
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) {
|
||||
return CUSTOM_EMOJI_REGEX.test(input);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useHistory } from 'react-router-dom';
|
||||
|
||||
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 PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { toggleStatusSpoilers } from 'mastodon/actions/statuses';
|
||||
@@ -96,8 +97,8 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
).size;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='notification-group__embedded-status animate-parent'
|
||||
<AnimateEmojiProvider
|
||||
className='notification-group__embedded-status'
|
||||
role='button'
|
||||
tabIndex={-1}
|
||||
onMouseDown={handleMouseDown}
|
||||
@@ -148,6 +149,6 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AnimateEmojiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,25 +2,19 @@ import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
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 ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react';
|
||||
import StarIcon from '@/material-icons/400-24px/star-fill.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 { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions';
|
||||
import { toggleFavourite } from 'mastodon/actions/interactions';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { BoostButton } from 'mastodon/components/status/boost_button';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
import type { Status } from 'mastodon/models/status';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
@@ -120,29 +114,6 @@ export const Footer: React.FC<{
|
||||
}
|
||||
}, [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(
|
||||
(e: React.MouseEvent) => {
|
||||
if (e.button !== 0 || !status) {
|
||||
@@ -160,13 +131,6 @@ export const Footer: React.FC<{
|
||||
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;
|
||||
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
@@ -179,24 +143,6 @@ export const Footer: React.FC<{
|
||||
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(
|
||||
status.get('favourited') ? messages.removeFavourite : messages.favourite,
|
||||
);
|
||||
@@ -222,19 +168,7 @@ export const Footer: React.FC<{
|
||||
counter={status.get('replies_count') as number}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
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)
|
||||
}
|
||||
/>
|
||||
<BoostButton counters status={status} />
|
||||
|
||||
<IconButton
|
||||
className='status__action-bar-button star-icon'
|
||||
|
||||
@@ -12,6 +12,8 @@ import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
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 Column from '../ui/components/column';
|
||||
@@ -31,9 +33,18 @@ export const Quotes: React.FC<{
|
||||
|
||||
const statusId = params?.statusId;
|
||||
|
||||
const { accountId: me } = useIdentity();
|
||||
|
||||
const isCorrectStatusId: boolean = useAppSelector(
|
||||
(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) =>
|
||||
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 (
|
||||
<Column bindToDocument={!multiColumn}>
|
||||
<ColumnHeader
|
||||
@@ -100,6 +137,7 @@ export const Quotes: React.FC<{
|
||||
isLoading={isLoading}
|
||||
emptyMessage={emptyMessage}
|
||||
bindToDocument={!multiColumn}
|
||||
prepend={prependMessage}
|
||||
/>
|
||||
|
||||
<Helmet>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { hydrateStore } from 'mastodon/actions/store';
|
||||
import { Router } from 'mastodon/components/router';
|
||||
import { DetailedStatus } from 'mastodon/features/status/components/detailed_status';
|
||||
import { useRenderSignal } from 'mastodon/hooks/useRenderSignal';
|
||||
import initialState from 'mastodon/initial_state';
|
||||
import { initialState } from 'mastodon/initial_state';
|
||||
import { IntlProvider } from 'mastodon/locales';
|
||||
import { makeGetStatus, makeGetPictureInPicture } from 'mastodon/selectors';
|
||||
import { store, useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||
|
||||
@@ -31,7 +31,7 @@ import { VisibilityIcon } from 'mastodon/components/visibility_icon';
|
||||
import { Audio } from 'mastodon/features/audio';
|
||||
import scheduleIdleTask from 'mastodon/features/ui/util/schedule_idle_task';
|
||||
import { Video } from 'mastodon/features/video';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
import { useIdentity } from 'mastodon/identity_context';
|
||||
|
||||
import Card from './card';
|
||||
|
||||
@@ -75,6 +75,8 @@ export const DetailedStatus: React.FC<{
|
||||
const [showDespiteFilter, setShowDespiteFilter] = useState(false);
|
||||
const nodeRef = useRef<HTMLDivElement>();
|
||||
|
||||
const { signedIn } = useIdentity();
|
||||
|
||||
const handleOpenVideo = useCallback(
|
||||
(options: VideoModalOptions) => {
|
||||
const lang = (status.getIn(['translation', 'language']) ||
|
||||
@@ -283,7 +285,7 @@ export const DetailedStatus: React.FC<{
|
||||
|
||||
if (['private', 'direct'].includes(status.get('visibility') as string)) {
|
||||
quotesLink = '';
|
||||
} else if (status.getIn(['account', 'id']) === me) {
|
||||
} else if (signedIn) {
|
||||
quotesLink = (
|
||||
<Link
|
||||
to={`/@${status.getIn(['account', 'acct'])}/${status.get('id')}/quotes`}
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface BaseConfirmationModalProps {
|
||||
export const ConfirmationModal: React.FC<
|
||||
{
|
||||
title: React.ReactNode;
|
||||
message: React.ReactNode;
|
||||
message?: React.ReactNode;
|
||||
confirm: React.ReactNode;
|
||||
cancel?: 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__confirmation'>
|
||||
<h1>{title}</h1>
|
||||
<p>{message}</p>
|
||||
{message && <p>{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ export {
|
||||
ConfirmReplyModal,
|
||||
ConfirmEditStatusModal,
|
||||
} from './discard_draft_confirmation';
|
||||
export { ConfirmWithdrawRequestModal } from './withdraw_follow_request';
|
||||
export { ConfirmUnfollowModal } from './unfollow';
|
||||
export { ConfirmUnblockModal } from './unblock';
|
||||
export { ConfirmClearNotificationsModal } from './clear_notifications';
|
||||
export { ConfirmLogOutModal } from './log_out';
|
||||
export { ConfirmFollowToListModal } from './follow_to_list';
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -10,10 +10,6 @@ import type { BaseConfirmationModalProps } from './confirmation_modal';
|
||||
import { ConfirmationModal } from './confirmation_modal';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollowTitle: {
|
||||
id: 'confirmations.unfollow.title',
|
||||
defaultMessage: 'Unfollow user?',
|
||||
},
|
||||
unfollowConfirm: {
|
||||
id: 'confirmations.unfollow.confirm',
|
||||
defaultMessage: 'Unfollow',
|
||||
@@ -34,12 +30,11 @@ export const ConfirmUnfollowModal: React.FC<
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
title={intl.formatMessage(messages.unfollowTitle)}
|
||||
message={
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='confirmations.unfollow.message'
|
||||
defaultMessage='Are you sure you want to unfollow {name}?'
|
||||
values={{ name: <strong>@{account.acct}</strong> }}
|
||||
id='confirmations.unfollow.title'
|
||||
defaultMessage='Unfollow {name}?'
|
||||
values={{ name: `@${account.acct}` }}
|
||||
/>
|
||||
}
|
||||
confirm={intl.formatMessage(messages.unfollowConfirm)}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -32,7 +32,9 @@ import {
|
||||
ConfirmDeleteListModal,
|
||||
ConfirmReplyModal,
|
||||
ConfirmEditStatusModal,
|
||||
ConfirmUnblockModal,
|
||||
ConfirmUnfollowModal,
|
||||
ConfirmWithdrawRequestModal,
|
||||
ConfirmClearNotificationsModal,
|
||||
ConfirmLogOutModal,
|
||||
ConfirmFollowToListModal,
|
||||
@@ -57,7 +59,9 @@ export const MODAL_COMPONENTS = {
|
||||
'CONFIRM_DELETE_LIST': () => Promise.resolve({ default: ConfirmDeleteListModal }),
|
||||
'CONFIRM_REPLY': () => Promise.resolve({ default: ConfirmReplyModal }),
|
||||
'CONFIRM_EDIT_STATUS': () => Promise.resolve({ default: ConfirmEditStatusModal }),
|
||||
'CONFIRM_UNBLOCK': () => Promise.resolve({ default: ConfirmUnblockModal }),
|
||||
'CONFIRM_UNFOLLOW': () => Promise.resolve({ default: ConfirmUnfollowModal }),
|
||||
'CONFIRM_WITHDRAW_REQUEST': () => Promise.resolve({ default: ConfirmWithdrawRequestModal }),
|
||||
'CONFIRM_CLEAR_NOTIFICATIONS': () => Promise.resolve({ default: ConfirmClearNotificationsModal }),
|
||||
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
|
||||
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
|
||||
|
||||
@@ -27,7 +27,7 @@ import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../act
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||
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 { NavigationBar } from './components/navigation_bar';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import initialState from '@/mastodon/initial_state';
|
||||
import { initialState } from '@/mastodon/initial_state';
|
||||
|
||||
interface FocusColumnOptions {
|
||||
index?: number;
|
||||
|
||||
@@ -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;
|
||||
141
app/javascript/mastodon/initial_state.ts
Normal file
141
app/javascript/mastodon/initial_state.ts
Normal 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');
|
||||
}
|
||||
@@ -133,7 +133,6 @@
|
||||
"confirmations.mute.confirm": "Silenciar",
|
||||
"confirmations.redraft.confirm": "Borrar y tornar ta borrador",
|
||||
"confirmations.unfollow.confirm": "Deixar de seguir",
|
||||
"confirmations.unfollow.message": "Yes seguro que quiers deixar de seguir a {name}?",
|
||||
"conversation.delete": "Borrar conversación",
|
||||
"conversation.mark_as_read": "Marcar como leyiu",
|
||||
"conversation.open": "Veyer conversación",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "لا يمكن التراجع عن هذا الإجراء.",
|
||||
"confirmations.revoke_quote.title": "أتريد إزالة المنشور؟",
|
||||
"confirmations.unfollow.confirm": "إلغاء المتابعة",
|
||||
"confirmations.unfollow.message": "متأكد من أنك تريد إلغاء متابعة {name} ؟",
|
||||
"confirmations.unfollow.title": "إلغاء متابعة المستخدم؟",
|
||||
"content_warning.hide": "إخفاء المنشور",
|
||||
"content_warning.show": "إظهار على أي حال",
|
||||
"content_warning.show_more": "إظهار المزيد",
|
||||
|
||||
@@ -155,8 +155,6 @@
|
||||
"confirmations.redraft.confirm": "Desaniciar y reeditar",
|
||||
"confirmations.redraft.title": "¿Desaniciar y reeditar la publicación?",
|
||||
"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.show": "Amosar de toes toes",
|
||||
"content_warning.show_more": "Amosar más",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.message": "Bu əməliyyatın geri dönüşü yoxdur.",
|
||||
"confirmations.revoke_quote.title": "Göndəriş silinsin?",
|
||||
"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.show": "Yenə də göstər",
|
||||
"content_warning.show_more": "Daha çox göstər",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.",
|
||||
"confirmations.revoke_quote.title": "Выдаліць допіс?",
|
||||
"confirmations.unfollow.confirm": "Адпісацца",
|
||||
"confirmations.unfollow.message": "Вы ўпэўненыя, што хочаце адпісацца ад {name}?",
|
||||
"confirmations.unfollow.title": "Адпісацца ад карыстальніка?",
|
||||
"content_warning.hide": "Схаваць допіс",
|
||||
"content_warning.show": "Усё адно паказаць",
|
||||
"content_warning.show_more": "Паказаць усё роўна",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.message": "Действието е неотменимо.",
|
||||
"confirmations.revoke_quote.title": "Премахвате ли публикацията?",
|
||||
"confirmations.unfollow.confirm": "Без следване",
|
||||
"confirmations.unfollow.message": "Наистина ли искате вече да не следвате {name}?",
|
||||
"confirmations.unfollow.title": "Спирате ли да следвате потребителя?",
|
||||
"content_warning.hide": "Скриване на публ.",
|
||||
"content_warning.show": "Нека се покаже",
|
||||
"content_warning.show_more": "Показване на още",
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"confirmations.mute.confirm": "সরিয়ে ফেলুন",
|
||||
"confirmations.redraft.confirm": "মুছে ফেলুন এবং আবার সম্পাদন করুন",
|
||||
"confirmations.unfollow.confirm": "অনুসরণ বন্ধ করো",
|
||||
"confirmations.unfollow.message": "তুমি কি নিশ্চিত {name} কে আর অনুসরণ করতে চাও না?",
|
||||
"conversation.delete": "কথোপকথন মুছে ফেলুন",
|
||||
"conversation.mark_as_read": "পঠিত হিসেবে চিহ্নিত করুন",
|
||||
"conversation.open": "কথপোকথন দেখান",
|
||||
|
||||
@@ -217,8 +217,6 @@
|
||||
"confirmations.revoke_quote.confirm": "Dilemel an embannadur",
|
||||
"confirmations.revoke_quote.title": "Dilemel an embannadur?",
|
||||
"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.show": "Diskwel memes tra",
|
||||
"content_warning.show_more": "Diskouez muioc'h",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.message": "Aquesta acció no es pot desfer.",
|
||||
"confirmations.revoke_quote.title": "Eliminar la publicació?",
|
||||
"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.show": "Mostra-la igualment",
|
||||
"content_warning.show_more": "Mostra'n més",
|
||||
|
||||
@@ -160,7 +160,6 @@
|
||||
"confirmations.redraft.confirm": "سڕینەوە & دووبارە ڕەشکردنەوە",
|
||||
"confirmations.redraft.message": "دڵنیای دەتەوێت ئەم پۆستە بسڕیتەوە و دووبارە دایبڕێژیتەوە؟ فەڤۆریت و بووستەکان لەدەست دەچن، وەڵامەکانی پۆستە ئەسڵیەکەش هەتیو دەبن.",
|
||||
"confirmations.unfollow.confirm": "بەدوادانەچو",
|
||||
"confirmations.unfollow.message": "ئایا دڵنیایت لەوەی دەتەوێت پەیڕەوی {name}?",
|
||||
"conversation.delete": "سڕینەوەی گفتوگۆ",
|
||||
"conversation.mark_as_read": "نیشانەکردن وەک خوێندراوە",
|
||||
"conversation.open": "نیشاندان گفتوگۆ",
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"confirmations.mute.confirm": "Piattà",
|
||||
"confirmations.redraft.confirm": "Sguassà è riscrive",
|
||||
"confirmations.unfollow.confirm": "Disabbunassi",
|
||||
"confirmations.unfollow.message": "Site sicuru·a ch'ùn vulete più siguità @{name}?",
|
||||
"conversation.delete": "Sguassà a cunversazione",
|
||||
"conversation.mark_as_read": "Marcà cum'è lettu",
|
||||
"conversation.open": "Vede a cunversazione",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Tuto akci nelze vrátit zpět.",
|
||||
"confirmations.revoke_quote.title": "Odstranit příspěvek?",
|
||||
"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.show": "Přesto zobrazit",
|
||||
"content_warning.show_more": "Zobrazit více",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Does dim modd dadwneud y weithred hon.",
|
||||
"confirmations.revoke_quote.title": "Dileu'r postiad?",
|
||||
"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.show": "Dangos beth bynnag",
|
||||
"content_warning.show_more": "Dangos rhagor",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Fjern indlæg",
|
||||
"confirmations.revoke_quote.message": "Denne handling kan ikke fortrydes.",
|
||||
"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.message": "Er du sikker på, at du ikke længere vil følge {name}?",
|
||||
"confirmations.unfollow.title": "Følg ikke længere bruger?",
|
||||
"confirmations.unfollow.title": "Følg ikke længere {name}?",
|
||||
"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.show": "Vis alligevel",
|
||||
"content_warning.show_more": "Vis flere",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Private indlæg kan ikke citeres",
|
||||
"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.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.reblog": "Fremhæv",
|
||||
"status.reblog_or_quote": "Fremhæv eller citér",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Beitrag entfernen",
|
||||
"confirmations.revoke_quote.message": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"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.message": "Möchtest du {name} wirklich entfolgen?",
|
||||
"confirmations.unfollow.title": "Profil entfolgen?",
|
||||
"confirmations.unfollow.title": "{name} 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.show": "Trotzdem anzeigen",
|
||||
"content_warning.show_more": "Beitrag anzeigen",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Private Beiträge können nicht zitiert werden",
|
||||
"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.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.reblog": "Teilen",
|
||||
"status.reblog_or_quote": "Teilen oder zitieren",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Αφαίρεση ανάρτησης",
|
||||
"confirmations.revoke_quote.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
|
||||
"confirmations.revoke_quote.title": "Αφαίρεση ανάρτησης;",
|
||||
"confirmations.unblock.confirm": "Άρση αποκλεισμού",
|
||||
"confirmations.unblock.title": "Άρση αποκλεισμού {name};",
|
||||
"confirmations.unfollow.confirm": "Άρση ακολούθησης",
|
||||
"confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};",
|
||||
"confirmations.unfollow.title": "Άρση ακολούθησης;",
|
||||
"confirmations.unfollow.title": "Κατάργηση ακολούθησης του/της {name};",
|
||||
"confirmations.withdraw_request.confirm": "Απόσυρση αιτήματος",
|
||||
"confirmations.withdraw_request.title": "Απόσυρση αιτήματος για να ακολουθήσετε τον/την {name};",
|
||||
"content_warning.hide": "Απόκρυψη ανάρτησης",
|
||||
"content_warning.show": "Εμφάνιση ούτως ή άλλως",
|
||||
"content_warning.show_more": "Εμφάνιση περισσότερων",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν",
|
||||
"status.quotes": "{count, plural, one {# παράθεση} other {# παραθέσεις}}",
|
||||
"status.quotes.empty": "Κανείς δεν έχει παραθέσει αυτή την ανάρτηση ακόμα. Μόλις το κάνει, θα εμφανιστεί εδώ.",
|
||||
"status.quotes.local_other_disclaimer": "Οι παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.",
|
||||
"status.quotes.remote_other_disclaimer": "Μόνο παραθέσεις από το {domain} είναι εγγυημένες ότι θα εμφανιστούν εδώ. Παραθέσεις που απορρίφθηκαν από τον συντάκτη δε θα εμφανιστούν.",
|
||||
"status.read_more": "Διάβασε περισότερα",
|
||||
"status.reblog": "Ενίσχυση",
|
||||
"status.reblog_or_quote": "Ενίσχυση ή παράθεση",
|
||||
|
||||
@@ -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.title": "Remove follower?",
|
||||
"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.show": "Show anyway",
|
||||
"content_warning.show_more": "Show more",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Remove post",
|
||||
"confirmations.revoke_quote.message": "This action cannot be undone.",
|
||||
"confirmations.revoke_quote.title": "Remove post?",
|
||||
"confirmations.unblock.confirm": "Unblock",
|
||||
"confirmations.unblock.title": "Unblock {name}?",
|
||||
"confirmations.unfollow.confirm": "Unfollow",
|
||||
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
|
||||
"confirmations.unfollow.title": "Unfollow user?",
|
||||
"confirmations.unfollow.title": "Unfollow {name}?",
|
||||
"confirmations.withdraw_request.confirm": "Withdraw request",
|
||||
"confirmations.withdraw_request.title": "Withdraw request to follow {name}?",
|
||||
"content_warning.hide": "Hide post",
|
||||
"content_warning.show": "Show anyway",
|
||||
"content_warning.show_more": "Show more",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Private posts cannot be quoted",
|
||||
"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.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.reblog": "Boost",
|
||||
"status.reblog_or_quote": "Boost or quote",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.message": "Ĉi tiu ago ne povas esti malfarita.",
|
||||
"confirmations.revoke_quote.title": "Ĉu forigi afiŝon?",
|
||||
"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.show": "Montri ĉiukaze",
|
||||
"content_warning.show_more": "Montri pli",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Eliminar mensaje",
|
||||
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
|
||||
"confirmations.revoke_quote.title": "¿Eliminar mensaje?",
|
||||
"confirmations.unblock.confirm": "Desbloquear",
|
||||
"confirmations.unblock.title": "¿Desbloquear a {name}?",
|
||||
"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 al usuario?",
|
||||
"confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
|
||||
"confirmations.withdraw_request.confirm": "Retirar solicitud",
|
||||
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
|
||||
"content_warning.hide": "Ocultar mensaje",
|
||||
"content_warning.show": "Mostrar de todos modos",
|
||||
"content_warning.show_more": "Mostrar más",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "No se pueden citar los mensajes privados",
|
||||
"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.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.reblog": "Adherir",
|
||||
"status.reblog_or_quote": "Adherir o citar",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Eliminar publicación",
|
||||
"confirmations.revoke_quote.message": "Esta acción no se puede deshacer.",
|
||||
"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.message": "¿Estás seguro de que quieres dejar de seguir a {name}?",
|
||||
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
|
||||
"confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
|
||||
"confirmations.withdraw_request.confirm": "Retirar solicitud",
|
||||
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
|
||||
"content_warning.hide": "Ocultar publicación",
|
||||
"content_warning.show": "Mostrar de todos modos",
|
||||
"content_warning.show_more": "Mostrar más",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Las publicaciones privadas no pueden citarse",
|
||||
"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.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.reblog": "Impulsar",
|
||||
"status.reblog_or_quote": "Impulsar o citar",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Eliminar publicación",
|
||||
"confirmations.revoke_quote.message": "Esta acción no tiene vuelta atrás.",
|
||||
"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.message": "¿Seguro que quieres dejar de seguir a {name}?",
|
||||
"confirmations.unfollow.title": "¿Dejar de seguir al usuario?",
|
||||
"confirmations.unfollow.title": "¿Dejar de seguir a {name}?",
|
||||
"confirmations.withdraw_request.confirm": "Retirar solicitud",
|
||||
"confirmations.withdraw_request.title": "¿Retirar solicitud de seguimiento a {name}?",
|
||||
"content_warning.hide": "Ocultar publicación",
|
||||
"content_warning.show": "Mostrar de todos modos",
|
||||
"content_warning.show_more": "Mostrar más",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Las publicaciones privadas no pueden ser citadas",
|
||||
"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.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.reblog": "Impulsar",
|
||||
"status.reblog_or_quote": "Impulsar o citar",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Seda tegevust ei saa tagasi pöörata.",
|
||||
"confirmations.revoke_quote.title": "Kas eemaldame postituse?",
|
||||
"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.show": "Näita ikkagi",
|
||||
"content_warning.show_more": "Näita rohkem",
|
||||
|
||||
@@ -240,8 +240,6 @@
|
||||
"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.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.show": "Erakutsi hala ere",
|
||||
"content_warning.show_more": "Erakutsi gehiago",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.message": "این اقدام قابل بازگشت نیست.",
|
||||
"confirmations.revoke_quote.title": "آیا فرسته را حذف کنم؟",
|
||||
"confirmations.unfollow.confirm": "پینگرفتن",
|
||||
"confirmations.unfollow.message": "مطمئنید که میخواهید به پیگیری از {name} پایان دهید؟",
|
||||
"confirmations.unfollow.title": "ناپیگیری کاربر؟",
|
||||
"content_warning.hide": "نهفتن فرسته",
|
||||
"content_warning.show": "در هر صورت نشان داده شود",
|
||||
"content_warning.show_more": "نمایش بیشتر",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Poista julkaisu",
|
||||
"confirmations.revoke_quote.message": "Tätä toimea ei voi peruuttaa.",
|
||||
"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.message": "Haluatko varmasti lopettaa profiilin {name} seuraamisen?",
|
||||
"confirmations.unfollow.title": "Lopetetaanko käyttäjän seuraaminen?",
|
||||
"confirmations.unfollow.title": "Lopetetaanko käyttäjän {name} 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.show": "Näytä kuitenkin",
|
||||
"content_warning.show_more": "Näytä lisää",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Yksityisiä julkaisuja ei voi lainata",
|
||||
"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.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.reblog": "Tehosta",
|
||||
"status.reblog_or_quote": "Tehosta tai lainaa",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Hendan atgerðin kann ikki angrast.",
|
||||
"confirmations.revoke_quote.title": "Strika post?",
|
||||
"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.show": "Vís kortini",
|
||||
"content_warning.show_more": "Vís meiri",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
|
||||
"confirmations.revoke_quote.title": "Retirer la publication ?",
|
||||
"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.show": "Montrer quand même",
|
||||
"content_warning.show_more": "Montrer plus",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "Cette action ne peut pas être annulée.",
|
||||
"confirmations.revoke_quote.title": "Retirer la publication ?",
|
||||
"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.show": "Montrer quand même",
|
||||
"content_warning.show_more": "Montrer plus",
|
||||
|
||||
@@ -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.title": "Folger fuortsmite?",
|
||||
"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.show": "Dochs toane",
|
||||
"content_warning.show_more": "Mear toane",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Ní féidir an gníomh seo a chealú.",
|
||||
"confirmations.revoke_quote.title": "Bain postáil?",
|
||||
"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.show": "Taispeáin ar aon nós",
|
||||
"content_warning.show_more": "Taispeáin níos mó",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"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.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.show": "Seall e co-dhiù",
|
||||
"content_warning.show_more": "Seall barrachd dheth",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Eliminar publicación",
|
||||
"confirmations.revoke_quote.message": "Esta acción non se pode desfacer.",
|
||||
"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.message": "Tes certeza de querer deixar de seguir a {name}?",
|
||||
"confirmations.unfollow.title": "Deixar de seguir á usuaria?",
|
||||
"confirmations.unfollow.title": "Deixa de seguir a {name}?",
|
||||
"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.show": "Mostrar igualmente",
|
||||
"content_warning.show_more": "Mostrar máis",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "As publicacións privadas non se poden citar",
|
||||
"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.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.reblog": "Promover",
|
||||
"status.reblog_or_quote": "Promover ou citar",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "הסרת הודעה",
|
||||
"confirmations.revoke_quote.message": "פעולה זו אינה הפיכה.",
|
||||
"confirmations.revoke_quote.title": "הסרת הודעה?",
|
||||
"confirmations.unblock.confirm": "הסרת חסימה",
|
||||
"confirmations.unblock.title": "הסרת חסימה מ־{name}?",
|
||||
"confirmations.unfollow.confirm": "הפסקת מעקב",
|
||||
"confirmations.unfollow.message": "להפסיק מעקב אחרי {name}?",
|
||||
"confirmations.unfollow.title": "לבטל מעקב אחר המשתמש.ת?",
|
||||
"confirmations.unfollow.title": "בטול מעקב אחרי {name}?",
|
||||
"confirmations.withdraw_request.confirm": "משיכת בקשה",
|
||||
"confirmations.withdraw_request.title": "משיכת בקשת מעקב אחרי {name}?",
|
||||
"content_warning.hide": "הסתרת חיצרוץ",
|
||||
"content_warning.show": "להציג בכל זאת",
|
||||
"content_warning.show_more": "הצג עוד",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "הודעות פרטיות לא ניתנות לציטוט",
|
||||
"status.quotes": "{count, plural,one {ציטוט}other {ציטוטים}}",
|
||||
"status.quotes.empty": "עוד לא ציטטו את ההודעה הזו. כאשר זה יקרה, הציטוטים יופיעו כאן.",
|
||||
"status.quotes.local_other_disclaimer": "ציטוטים שיידחו על ידי המחברים המקוריים לא יוצגו.",
|
||||
"status.quotes.remote_other_disclaimer": "רק ציטוטים מהשרת {domain} מובטחים שיופיעו פה. ציטוטים שנדחו על ידי המצוטטים לא יופיעו.",
|
||||
"status.read_more": "לקרוא עוד",
|
||||
"status.reblog": "הדהוד",
|
||||
"status.reblog_or_quote": "להדהד או לצטט",
|
||||
|
||||
@@ -168,7 +168,6 @@
|
||||
"confirmations.redraft.confirm": "मिटायें और पुनःप्रारूपण करें",
|
||||
"confirmations.redraft.message": "क्या आप वाकई इस स्टेटस को हटाना चाहते हैं और इसे फिर से ड्राफ्ट करना चाहते हैं? पसंदीदा और बूस्ट खो जाएंगे, और मूल पोस्ट के उत्तर अनाथ हो जाएंगे।",
|
||||
"confirmations.unfollow.confirm": "अनफॉलो करें",
|
||||
"confirmations.unfollow.message": "क्या आप वाकई {name} को अनफॉलो करना चाहते हैं?",
|
||||
"conversation.delete": "वार्तालाप हटाएँ",
|
||||
"conversation.mark_as_read": "पढ़ा गया के रूप में चिह्नित करें",
|
||||
"conversation.open": "वार्तालाप देखें",
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
"confirmations.mute.confirm": "Utišaj",
|
||||
"confirmations.redraft.confirm": "Izbriši i ponovno uredi",
|
||||
"confirmations.unfollow.confirm": "Prestani pratiti",
|
||||
"confirmations.unfollow.message": "Jeste li sigurni da želite prestati pratiti {name}?",
|
||||
"conversation.delete": "Izbriši razgovor",
|
||||
"conversation.mark_as_read": "Označi kao pročitano",
|
||||
"conversation.open": "Prikaži razgovor",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "Ez a művelet nem vonható vissza.",
|
||||
"confirmations.revoke_quote.title": "Bejegyzés eltávolítá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.show": "Megjelenítés mindenképp",
|
||||
"content_warning.show_more": "Több megjelenítése",
|
||||
|
||||
@@ -124,7 +124,6 @@
|
||||
"confirmations.mute.confirm": "Լռեցնել",
|
||||
"confirmations.redraft.confirm": "Ջնջել եւ խմբագրել նորից",
|
||||
"confirmations.unfollow.confirm": "Ապահետեւել",
|
||||
"confirmations.unfollow.message": "Վստա՞հ ես, որ ուզում ես այլեւս չհետեւել {name}֊ին։",
|
||||
"conversation.delete": "Ջնջել խօսակցութիւնը",
|
||||
"conversation.mark_as_read": "Նշել որպէս ընթերցուած",
|
||||
"conversation.open": "Դիտել խօսակցութիւնը",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Iste action non pote esser disfacite.",
|
||||
"confirmations.revoke_quote.title": "Remover message?",
|
||||
"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.show": "Monstrar in omne caso",
|
||||
"content_warning.show_more": "Monstrar plus",
|
||||
|
||||
@@ -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.title": "Delete & redraft post?",
|
||||
"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.show": "Show anyway",
|
||||
"conversation.delete": "Hapus percakapan",
|
||||
|
||||
@@ -168,7 +168,6 @@
|
||||
"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.unfollow.confirm": "Dessequer",
|
||||
"confirmations.unfollow.message": "Esque tu vermen vole dessequer {name}?",
|
||||
"conversation.delete": "Deleter conversation",
|
||||
"conversation.mark_as_read": "Marcar quam leet",
|
||||
"conversation.open": "Vider conversation",
|
||||
|
||||
@@ -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.title": "Ka efacar & riskisar posto?",
|
||||
"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.show": "Montrez nur",
|
||||
"content_warning.show_more": "Montrar plu",
|
||||
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Fjarlægja færslu",
|
||||
"confirmations.revoke_quote.message": "Þessa aðgerð er ekki hægt að afturkalla.",
|
||||
"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.message": "Ertu viss um að þú viljir hætta að fylgjast með {name}?",
|
||||
"confirmations.unfollow.title": "Hætta að fylgjast með viðkomandi?",
|
||||
"confirmations.unfollow.title": "Hætta að fylgjast með {name}?",
|
||||
"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.show": "Birta samt",
|
||||
"content_warning.show_more": "Sýna meira",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Ekki er hægt að vitna í einkafærslur",
|
||||
"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.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.reblog": "Endurbirting",
|
||||
"status.reblog_or_quote": "Endurbirta eða vitna í færslu",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "Questa azione non può essere annullata.",
|
||||
"confirmations.revoke_quote.title": "Rimuovere il post?",
|
||||
"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.show": "Mostra comunque",
|
||||
"content_warning.show_more": "Mostra di più",
|
||||
|
||||
@@ -248,8 +248,6 @@
|
||||
"confirmations.revoke_quote.confirm": "投稿を削除",
|
||||
"confirmations.revoke_quote.title": "投稿を削除しますか?",
|
||||
"confirmations.unfollow.confirm": "フォロー解除",
|
||||
"confirmations.unfollow.message": "本当に{name}さんのフォローを解除しますか?",
|
||||
"confirmations.unfollow.title": "フォローを解除しようとしています",
|
||||
"content_warning.hide": "内容を隠す",
|
||||
"content_warning.show": "承知して表示",
|
||||
"content_warning.show_more": "続きを表示",
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
"confirmations.mute.confirm": "დადუმება",
|
||||
"confirmations.redraft.confirm": "გაუქმება და გადანაწილება",
|
||||
"confirmations.unfollow.confirm": "ნუღარ მიჰყვები",
|
||||
"confirmations.unfollow.message": "დარწმუნებული ხართ, აღარ გსურთ მიჰყვებოდეთ {name}-ს?",
|
||||
"embed.instructions": "ეს სტატუსი ჩასვით თქვენს ვებ-საიტზე შემდეგი კოდის კოპირებით.",
|
||||
"embed.preview": "ესაა თუ როგორც გამოჩნდება:",
|
||||
"emoji_button.activity": "აქტივობა",
|
||||
|
||||
@@ -194,7 +194,6 @@
|
||||
"confirmations.revoke_quote.confirm": "Kkes tasuffeɣt",
|
||||
"confirmations.revoke_quote.title": "Kkes tasuffeɣt?",
|
||||
"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.show": "Ssken-d akken tebɣu tili",
|
||||
"content_warning.show_more": "Sken-d ugar",
|
||||
|
||||
@@ -140,7 +140,6 @@
|
||||
"confirmations.mute.confirm": "Үнсіз қылу",
|
||||
"confirmations.redraft.confirm": "Өшіруді құптау",
|
||||
"confirmations.unfollow.confirm": "Оқымау",
|
||||
"confirmations.unfollow.message": "\"{name} атты қолданушыға енді жазылғыңыз келмей ме?",
|
||||
"conversation.delete": "Пікірталасты өшіру",
|
||||
"conversation.mark_as_read": "Оқылды деп белгіле",
|
||||
"conversation.open": "Пікірталасты қарау",
|
||||
|
||||
@@ -252,8 +252,6 @@
|
||||
"confirmations.revoke_quote.message": "이 작업은 되돌릴 수 없습니다.",
|
||||
"confirmations.revoke_quote.title": "게시물을 지울까요?",
|
||||
"confirmations.unfollow.confirm": "팔로우 해제",
|
||||
"confirmations.unfollow.message": "정말로 {name} 님을 팔로우 해제하시겠습니까?",
|
||||
"confirmations.unfollow.title": "사용자를 언팔로우 할까요?",
|
||||
"content_warning.hide": "게시물 숨기기",
|
||||
"content_warning.show": "무시하고 보기",
|
||||
"content_warning.show_more": "더 보기",
|
||||
|
||||
@@ -169,7 +169,6 @@
|
||||
"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.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",
|
||||
"conversation.delete": "Axaftinê jê bibe",
|
||||
"conversation.mark_as_read": "Wekî xwendî nîşan bide",
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
"confirmations.mute.confirm": "Tawhe",
|
||||
"confirmations.redraft.confirm": "Dilea & daskynskrifa",
|
||||
"confirmations.unfollow.confirm": "Anholya",
|
||||
"confirmations.unfollow.message": "Owgh hwi sur a vynnes anholya {name}?",
|
||||
"conversation.delete": "Dilea kesklapp",
|
||||
"conversation.mark_as_read": "Merkya vel redys",
|
||||
"conversation.open": "Gweles kesklapp",
|
||||
|
||||
@@ -214,8 +214,6 @@
|
||||
"confirmations.revoke_quote.confirm": "Kita puvlikasyon",
|
||||
"confirmations.revoke_quote.title": "Kitar puvlikasyon?",
|
||||
"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.show": "Amostra entanto",
|
||||
"content_warning.show_more": "Amostra mas",
|
||||
|
||||
@@ -232,8 +232,6 @@
|
||||
"confirmations.remove_from_followers.message": "{name} nustos jus sekti. Ar tikrai norite tęsti?",
|
||||
"confirmations.remove_from_followers.title": "Šalinti sekėją?",
|
||||
"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.show": "Rodyti vis tiek",
|
||||
"content_warning.show_more": "Rodyti daugiau",
|
||||
|
||||
@@ -243,8 +243,6 @@
|
||||
"confirmations.revoke_quote.message": "Šo darbību nevar atsaukt.",
|
||||
"confirmations.revoke_quote.title": "Noņemt ierakstu?",
|
||||
"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.show": "Tomēr rādīt",
|
||||
"content_warning.show_more": "Rādīt vairāk",
|
||||
|
||||
@@ -82,7 +82,6 @@
|
||||
"confirmations.logout.message": "Дали сте сигурни дека сакате да се одјавите?",
|
||||
"confirmations.mute.confirm": "Заќути",
|
||||
"confirmations.unfollow.confirm": "Одследи",
|
||||
"confirmations.unfollow.message": "Сигурни сте дека ќе го отследите {name}?",
|
||||
"conversation.delete": "Избриши разговор",
|
||||
"conversation.mark_as_read": "Означете како прочитано",
|
||||
"conversation.open": "Прегледај разговор",
|
||||
|
||||
@@ -139,7 +139,6 @@
|
||||
"confirmations.mute.confirm": "നിശ്ശബ്ദമാക്കുക",
|
||||
"confirmations.redraft.confirm": "മായിച്ച് മാറ്റങ്ങൾ വരുത്തി വീണ്ടും എഴുതുക",
|
||||
"confirmations.unfollow.confirm": "പിന്തുടരുന്നത് നിര്ത്തുക",
|
||||
"confirmations.unfollow.message": "നിങ്ങൾ {name} യെ പിന്തുടരുന്നത് നിർത്തുവാൻ തീർച്ചയായും തീരുമാനിച്ചുവോ?",
|
||||
"conversation.delete": "സംഭാഷണം മായിക്കുക",
|
||||
"conversation.mark_as_read": "വായിച്ചതായി അടയാളപ്പെടുത്തുക",
|
||||
"conversation.open": "സംഭാഷണം കാണുക",
|
||||
|
||||
@@ -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.title": "Padam & gubah semula hantaran?",
|
||||
"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.show": "Tunjuk saja",
|
||||
"content_warning.show_more": "Tunjuk lebih",
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"confirmations.redraft.confirm": "ဖျက်ပြီး ပြန်လည်ရေးမည်။",
|
||||
"confirmations.redraft.message": "သင် ဒီပိုစ့်ကိုဖျက်ပြီး ပြန်တည်းဖြတ်မှာ သေချာပြီလား။ ကြယ်ပွင့်တွေ နဲ့ ပြန်မျှဝေမှုတွေကိုဆုံးရှုံးမည်။မူရင်းပို့စ်ဆီကို ပြန်စာတွေမှာလည်း \nပိုစ့်ကိုတွေ့ရမည်မဟုတ်တော့ပါ။.",
|
||||
"confirmations.unfollow.confirm": "စောင့်ကြည့်ခြင်းအား ပယ်ဖျက်မည်",
|
||||
"confirmations.unfollow.message": "{name} ကို စောင်ကြည့်ခြင်းအား ပယ်ဖျက်မည်မှာသေချာပါသလား။",
|
||||
"conversation.delete": "ဤစကားပြောဆိုမှုကို ဖျက်ပစ်မည်",
|
||||
"conversation.mark_as_read": "ဖတ်ပြီးသားအဖြစ်မှတ်ထားပါ",
|
||||
"conversation.open": "Conversation ကိုကြည့်မည်",
|
||||
|
||||
@@ -258,8 +258,6 @@
|
||||
"confirmations.revoke_quote.message": "Tsit ê動作bē當復原。",
|
||||
"confirmations.revoke_quote.title": "Kám beh thâi掉PO文?",
|
||||
"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.show": "Mā tio̍h顯示",
|
||||
"content_warning.show_more": "其他內容",
|
||||
|
||||
@@ -143,8 +143,6 @@
|
||||
"confirmations.redraft.confirm": "मेटाएर पुन: ड्राफ्ट गर्नुहोस्",
|
||||
"confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?",
|
||||
"confirmations.unfollow.confirm": "अनफलो गर्नुहोस्",
|
||||
"confirmations.unfollow.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ {name}लाई अनफलो गर्न चाहनुहुन्छ?",
|
||||
"confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने?",
|
||||
"disabled_account_banner.account_settings": "खाता सेटिङहरू",
|
||||
"empty_column.direct": "तपाईंले अहिलेसम्म कुनै पनि प्राइवेट उल्लेखहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",
|
||||
"empty_column.follow_requests": "तपाईंले अहिलेसम्म कुनै पनि फलो अनुरोधहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"account.follow": "Volgen",
|
||||
"account.follow_back": "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_short": "Annuleren",
|
||||
"account.follow_request_short": "Verzoek",
|
||||
@@ -257,9 +257,12 @@
|
||||
"confirmations.revoke_quote.confirm": "Bericht verwijderen",
|
||||
"confirmations.revoke_quote.message": "Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"confirmations.revoke_quote.title": "Bericht verwijderen?",
|
||||
"confirmations.unblock.confirm": "Deblokkeren",
|
||||
"confirmations.unblock.title": "{name} deblokkeren?",
|
||||
"confirmations.unfollow.confirm": "Ontvolgen",
|
||||
"confirmations.unfollow.message": "Weet je het zeker dat je {name} wilt ontvolgen?",
|
||||
"confirmations.unfollow.title": "Gebruiker ontvolgen?",
|
||||
"confirmations.unfollow.title": "{name} ontvolgen?",
|
||||
"confirmations.withdraw_request.confirm": "Verzoek intrekken",
|
||||
"confirmations.withdraw_request.title": "Verzoek intrekken om {name} te volgen?",
|
||||
"content_warning.hide": "Bericht verbergen",
|
||||
"content_warning.show": "Alsnog tonen",
|
||||
"content_warning.show_more": "Meer tonen",
|
||||
@@ -920,6 +923,8 @@
|
||||
"status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk",
|
||||
"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.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.reblog": "Boosten",
|
||||
"status.reblog_or_quote": "Boosten of citeren",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user