Merge commit '28be5a199f685b9fb26742c4d78ddca53df05d57' into glitch-soc/merge-upstream
This commit is contained in:
commit
b15b07606c
2
Gemfile
2
Gemfile
@ -4,7 +4,7 @@ source 'https://rubygems.org'
|
|||||||
ruby '>= 3.2.0', '< 3.5.0'
|
ruby '>= 3.2.0', '< 3.5.0'
|
||||||
|
|
||||||
gem 'propshaft'
|
gem 'propshaft'
|
||||||
gem 'puma', '~> 6.3'
|
gem 'puma', '~> 7.0'
|
||||||
gem 'rails', '~> 8.0'
|
gem 'rails', '~> 8.0'
|
||||||
gem 'thor', '~> 1.2'
|
gem 'thor', '~> 1.2'
|
||||||
|
|
||||||
|
|||||||
@ -637,7 +637,7 @@ GEM
|
|||||||
date
|
date
|
||||||
stringio
|
stringio
|
||||||
public_suffix (6.0.2)
|
public_suffix (6.0.2)
|
||||||
puma (6.6.1)
|
puma (7.0.3)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
pundit (2.5.1)
|
pundit (2.5.1)
|
||||||
activesupport (>= 3.0.0)
|
activesupport (>= 3.0.0)
|
||||||
@ -1052,7 +1052,7 @@ DEPENDENCIES
|
|||||||
prometheus_exporter (~> 2.2)
|
prometheus_exporter (~> 2.2)
|
||||||
propshaft
|
propshaft
|
||||||
public_suffix (~> 6.0)
|
public_suffix (~> 6.0)
|
||||||
puma (~> 6.3)
|
puma (~> 7.0)
|
||||||
pundit (~> 2.3)
|
pundit (~> 2.3)
|
||||||
rack-attack (~> 6.6)
|
rack-attack (~> 6.6)
|
||||||
rack-cors
|
rack-cors
|
||||||
|
|||||||
@ -4,7 +4,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
|
|||||||
include Api::InteractionPoliciesConcern
|
include Api::InteractionPoliciesConcern
|
||||||
|
|
||||||
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
|
||||||
before_action -> { check_feature_enabled }
|
|
||||||
|
|
||||||
def update
|
def update
|
||||||
authorize @status, :update?
|
authorize @status, :update?
|
||||||
@ -22,10 +21,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
|
|||||||
params.permit(:quote_approval_policy)
|
params.permit(:quote_approval_policy)
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_feature_enabled
|
|
||||||
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
|
|
||||||
end
|
|
||||||
|
|
||||||
def broadcast_updates!
|
def broadcast_updates!
|
||||||
DistributionWorker.perform_async(@status.id, { 'update' => true })
|
DistributionWorker.perform_async(@status.id, { 'update' => true })
|
||||||
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })
|
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })
|
||||||
|
|||||||
@ -159,8 +159,6 @@ class Api::V1::StatusesController < Api::BaseController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def set_quoted_status
|
def set_quoted_status
|
||||||
return unless Mastodon::Feature.outgoing_quotes_enabled?
|
|
||||||
|
|
||||||
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
|
||||||
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
authorize(@quoted_status, :quote?) if @quoted_status.present?
|
||||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||||
|
|||||||
@ -4,8 +4,6 @@ module Api::InteractionPoliciesConcern
|
|||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
def quote_approval_policy
|
def quote_approval_policy
|
||||||
return nil unless Mastodon::Feature.outgoing_quotes_enabled?
|
|
||||||
|
|
||||||
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
|
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
|
||||||
when 'public'
|
when 'public'
|
||||||
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
|
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const meta = {
|
|||||||
component: Alert,
|
component: Alert,
|
||||||
args: {
|
args: {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
isLoading: false,
|
||||||
animateFrom: 'side',
|
animateFrom: 'side',
|
||||||
title: '',
|
title: '',
|
||||||
message: '',
|
message: '',
|
||||||
@ -20,6 +21,12 @@ const meta = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: 'Animate to the active (displayed) state of the alert',
|
description: 'Animate to the active (displayed) state of the alert',
|
||||||
},
|
},
|
||||||
|
isLoading: {
|
||||||
|
control: 'boolean',
|
||||||
|
type: 'boolean',
|
||||||
|
description:
|
||||||
|
'Display a loading indicator in the alert, replacing the dismiss button if present',
|
||||||
|
},
|
||||||
animateFrom: {
|
animateFrom: {
|
||||||
control: 'radio',
|
control: 'radio',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
@ -108,3 +115,11 @@ export const InSizedContainer: Story = {
|
|||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const WithLoadingIndicator: Story = {
|
||||||
|
args: {
|
||||||
|
...WithDismissButton.args,
|
||||||
|
isLoading: true,
|
||||||
|
},
|
||||||
|
render: InSizedContainer.render,
|
||||||
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useIntl } from 'react-intl';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
|
||||||
import { IconButton } from '../icon_button';
|
import { IconButton } from '../icon_button';
|
||||||
|
|
||||||
@ -10,21 +11,23 @@ import { IconButton } from '../icon_button';
|
|||||||
* Snackbar/Toast-style notification component.
|
* Snackbar/Toast-style notification component.
|
||||||
*/
|
*/
|
||||||
export const Alert: React.FC<{
|
export const Alert: React.FC<{
|
||||||
isActive?: boolean;
|
|
||||||
animateFrom?: 'side' | 'below';
|
|
||||||
title?: string;
|
title?: string;
|
||||||
message: string;
|
message: string;
|
||||||
action?: string;
|
action?: string;
|
||||||
onActionClick?: () => void;
|
onActionClick?: () => void;
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
|
isActive?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
animateFrom?: 'side' | 'below';
|
||||||
}> = ({
|
}> = ({
|
||||||
isActive,
|
|
||||||
animateFrom = 'side',
|
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
action,
|
action,
|
||||||
onActionClick,
|
onActionClick,
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
isActive,
|
||||||
|
isLoading,
|
||||||
|
animateFrom = 'side',
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@ -51,7 +54,13 @@ export const Alert: React.FC<{
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{onDismiss && (
|
{isLoading && (
|
||||||
|
<span className='notification-bar__loading-indicator'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{onDismiss && !isLoading && (
|
||||||
<IconButton
|
<IconButton
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
id: 'dismissable_banner.dismiss',
|
id: 'dismissable_banner.dismiss',
|
||||||
|
|||||||
@ -14,7 +14,10 @@ export const DisplayNameWithoutDomain: FC<
|
|||||||
ComponentPropsWithoutRef<'span'>
|
ComponentPropsWithoutRef<'span'>
|
||||||
> = ({ account, className, children, ...props }) => {
|
> = ({ account, className, children, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<span {...props} className={classNames('display-name', className)}>
|
<span
|
||||||
|
{...props}
|
||||||
|
className={classNames('display-name animate-parent', className)}
|
||||||
|
>
|
||||||
<bdi>
|
<bdi>
|
||||||
{account ? (
|
{account ? (
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper component for managing the rendering of components that
|
||||||
|
* need to stay in the DOM a bit longer to finish their CSS exit animation.
|
||||||
|
*
|
||||||
|
* In the future, replace this component with plain CSS once that is feasible.
|
||||||
|
* This will require broader support for `transition-behavior: allow-discrete`
|
||||||
|
* and https://developer.mozilla.org/en-US/docs/Web/CSS/overlay.
|
||||||
|
*/
|
||||||
|
export const ExitAnimationWrapper: React.FC<{
|
||||||
|
/**
|
||||||
|
* Set this to true to indicate that the nested component should be rendered
|
||||||
|
*/
|
||||||
|
isActive: boolean;
|
||||||
|
/**
|
||||||
|
* How long the component should be rendered after `isActive` was set to `false`
|
||||||
|
*/
|
||||||
|
delayMs?: number;
|
||||||
|
/**
|
||||||
|
* Set this to true to also delay the entry of the nested component until after
|
||||||
|
* another one has exited full.
|
||||||
|
*/
|
||||||
|
withEntryDelay?: boolean;
|
||||||
|
/**
|
||||||
|
* Render prop that provides the nested component with the `delayedIsActive` flag
|
||||||
|
*/
|
||||||
|
children: (delayedIsActive: boolean) => React.ReactNode;
|
||||||
|
}> = ({ isActive = false, delayMs = 500, withEntryDelay, children }) => {
|
||||||
|
const [delayedIsActive, setDelayedIsActive] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive && !withEntryDelay) {
|
||||||
|
setDelayedIsActive(true);
|
||||||
|
|
||||||
|
return () => '';
|
||||||
|
} else {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setDelayedIsActive(isActive);
|
||||||
|
}, delayMs);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [isActive, delayMs, withEntryDelay]);
|
||||||
|
|
||||||
|
if (!isActive && !delayedIsActive) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children(isActive && delayedIsActive);
|
||||||
|
};
|
||||||
@ -118,6 +118,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
unread: PropTypes.bool,
|
unread: PropTypes.bool,
|
||||||
showThread: PropTypes.bool,
|
showThread: PropTypes.bool,
|
||||||
isQuotedPost: PropTypes.bool,
|
isQuotedPost: PropTypes.bool,
|
||||||
|
shouldHighlightOnMount: PropTypes.bool,
|
||||||
getScrollPosition: PropTypes.func,
|
getScrollPosition: PropTypes.func,
|
||||||
updateScrollBottom: PropTypes.func,
|
updateScrollBottom: PropTypes.func,
|
||||||
cacheMediaWidth: PropTypes.func,
|
cacheMediaWidth: PropTypes.func,
|
||||||
@ -567,6 +568,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted,
|
'status--first-in-thread': previousId && (!connectUp || connectToRoot), muted: this.props.muted,
|
||||||
'status--is-quote': isQuotedPost,
|
'status--is-quote': isQuotedPost,
|
||||||
'status--has-quote': !!status.get('quote'),
|
'status--has-quote': !!status.get('quote'),
|
||||||
|
'status--highlighted-entry': this.props.shouldHighlightOnMount,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
data-id={status.get('id')}
|
data-id={status.get('id')}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
|
|||||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||||
import { statusFactoryState } from '@/testing/factories';
|
import { statusFactoryState } from '@/testing/factories';
|
||||||
|
|
||||||
import { LegacyReblogButton, StatusBoostButton } from './boost_button';
|
import { BoostButton } from './boost_button';
|
||||||
|
|
||||||
interface StoryProps {
|
interface StoryProps {
|
||||||
visibility: StatusVisibility;
|
visibility: StatusVisibility;
|
||||||
@ -38,10 +38,7 @@ const meta = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
render: (args) => (
|
render: (args) => (
|
||||||
<StatusBoostButton
|
<BoostButton status={argsToStatus(args)} counters={args.reblogCount > 0} />
|
||||||
status={argsToStatus(args)}
|
|
||||||
counters={args.reblogCount > 0}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
} satisfies Meta<StoryProps>;
|
} satisfies Meta<StoryProps>;
|
||||||
|
|
||||||
@ -78,12 +75,3 @@ export const Mine: Story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Legacy: Story = {
|
|
||||||
render: (args) => (
|
|
||||||
<LegacyReblogButton
|
|
||||||
status={argsToStatus(args)}
|
|
||||||
counters={args.reblogCount > 0}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react';
|
import type { FC, KeyboardEvent, MouseEvent } from 'react';
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
@ -11,7 +11,6 @@ import { openModal } from '@/mastodon/actions/modal';
|
|||||||
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
|
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
|
||||||
import type { Status } from '@/mastodon/models/status';
|
import type { Status } from '@/mastodon/models/status';
|
||||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
import { isFeatureEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import type { SomeRequired } from '@/mastodon/utils/types';
|
import type { SomeRequired } from '@/mastodon/utils/types';
|
||||||
|
|
||||||
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
|
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
|
||||||
@ -47,10 +46,7 @@ interface ReblogButtonProps {
|
|||||||
|
|
||||||
type ActionMenuItemWithIcon = SomeRequired<ActionMenuItem, 'icon'>;
|
type ActionMenuItemWithIcon = SomeRequired<ActionMenuItem, 'icon'>;
|
||||||
|
|
||||||
export const StatusBoostButton: FC<ReblogButtonProps> = ({
|
export const BoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
|
||||||
status,
|
|
||||||
counters,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const statusState = useAppSelector((state) =>
|
const statusState = useAppSelector((state) =>
|
||||||
@ -192,65 +188,3 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Legacy helpers
|
|
||||||
|
|
||||||
// Switch between the legacy and new reblog button based on feature flag.
|
|
||||||
export const BoostButton: FC<ReblogButtonProps> = (props) => {
|
|
||||||
if (isFeatureEnabled('outgoing_quotes')) {
|
|
||||||
return <StatusBoostButton {...props} />;
|
|
||||||
}
|
|
||||||
return <LegacyReblogButton {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LegacyReblogButton: FC<ReblogButtonProps> = ({
|
|
||||||
status,
|
|
||||||
counters,
|
|
||||||
}) => {
|
|
||||||
const intl = useIntl();
|
|
||||||
const statusState = useAppSelector((state) =>
|
|
||||||
selectStatusState(state, status),
|
|
||||||
);
|
|
||||||
|
|
||||||
const { title, meta, iconComponent, disabled } = useMemo(
|
|
||||||
() => boostItemState(statusState),
|
|
||||||
[statusState],
|
|
||||||
);
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const handleClick: MouseEventHandler = useCallback(
|
|
||||||
(event) => {
|
|
||||||
if (statusState.isLoggedIn) {
|
|
||||||
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
|
|
||||||
} else {
|
|
||||||
dispatch(
|
|
||||||
openModal({
|
|
||||||
modalType: 'INTERACTION',
|
|
||||||
modalProps: {
|
|
||||||
accountId: status.getIn(['account', 'id']),
|
|
||||||
url: status.get('uri'),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[dispatch, status, statusState.isLoggedIn],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
disabled={disabled}
|
|
||||||
active={!!status.get('reblogged')}
|
|
||||||
title={intl.formatMessage(meta ?? title)}
|
|
||||||
icon='retweet'
|
|
||||||
iconComponent={iconComponent}
|
|
||||||
onClick={!disabled ? handleClick : undefined}
|
|
||||||
counter={
|
|
||||||
counters
|
|
||||||
? (status.get('reblogs_count') as number) +
|
|
||||||
(status.get('quotes_count') as number)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import { Dropdown } from 'mastodon/components/dropdown_menu';
|
|||||||
import { me } from '../../initial_state';
|
import { me } from '../../initial_state';
|
||||||
|
|
||||||
import { IconButton } from '../icon_button';
|
import { IconButton } from '../icon_button';
|
||||||
import { isFeatureEnabled } from '../../utils/environment';
|
|
||||||
import { BoostButton } from '../status/boost_button';
|
import { BoostButton } from '../status/boost_button';
|
||||||
import { RemoveQuoteHint } from './remove_quote_hint';
|
import { RemoveQuoteHint } from './remove_quote_hint';
|
||||||
|
|
||||||
@ -281,7 +280,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
|||||||
|
|
||||||
if (writtenByMe || withDismiss) {
|
if (writtenByMe || withDismiss) {
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
|
if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) {
|
||||||
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
||||||
}
|
}
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|||||||
@ -140,32 +140,6 @@ class StatusContent extends PureComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseEnter = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-original');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseLeave = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-static');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this._updateStatusLinks();
|
this._updateStatusLinks();
|
||||||
}
|
}
|
||||||
@ -257,7 +231,13 @@ class StatusContent extends PureComponent {
|
|||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={classNames} ref={this.setRef} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div
|
||||||
|
className={classNames}
|
||||||
|
ref={this.setRef}
|
||||||
|
onMouseDown={this.handleMouseDown}
|
||||||
|
onMouseUp={this.handleMouseUp}
|
||||||
|
key='status-content'
|
||||||
|
>
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
className='status__content__text status__content__text--visible translate'
|
className='status__content__text status__content__text--visible translate'
|
||||||
lang={language}
|
lang={language}
|
||||||
@ -274,7 +254,7 @@ class StatusContent extends PureComponent {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<div className={classNames} ref={this.setRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
<div className={classNames} ref={this.setRef}>
|
||||||
<EmojiHTML
|
<EmojiHTML
|
||||||
className='status__content__text status__content__text--visible translate'
|
className='status__content__text status__content__text--visible translate'
|
||||||
lang={language}
|
lang={language}
|
||||||
|
|||||||
@ -47,8 +47,6 @@ import Status from '../components/status';
|
|||||||
import { deleteModal } from '../initial_state';
|
import { deleteModal } from '../initial_state';
|
||||||
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
|
||||||
|
|
||||||
import { isFeatureEnabled } from 'mastodon/utils/environment';
|
|
||||||
|
|
||||||
const makeMapStateToProps = () => {
|
const makeMapStateToProps = () => {
|
||||||
const getStatus = makeGetStatus();
|
const getStatus = makeGetStatus();
|
||||||
const getPictureInPicture = makeGetPictureInPicture();
|
const getPictureInPicture = makeGetPictureInPicture();
|
||||||
@ -81,9 +79,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
onQuote (status) {
|
onQuote (status) {
|
||||||
if (isFeatureEnabled('outgoing_quotes')) {
|
dispatch(quoteComposeById(status.get('id')));
|
||||||
dispatch(quoteComposeById(status.get('id')));
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onFavourite (status) {
|
onFavourite (status) {
|
||||||
|
|||||||
@ -379,36 +379,6 @@ export const AccountHeader: React.FC<{
|
|||||||
});
|
});
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(
|
|
||||||
({ currentTarget }: React.MouseEvent) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTarget
|
|
||||||
.querySelectorAll<HTMLImageElement>('.custom-emoji')
|
|
||||||
.forEach((emoji) => {
|
|
||||||
emoji.src = emoji.getAttribute('data-original') ?? '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(
|
|
||||||
({ currentTarget }: React.MouseEvent) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentTarget
|
|
||||||
.querySelectorAll<HTMLImageElement>('.custom-emoji')
|
|
||||||
.forEach((emoji) => {
|
|
||||||
emoji.src = emoji.getAttribute('data-static') ?? '';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const suspended = account?.suspended;
|
const suspended = account?.suspended;
|
||||||
const isRemote = account?.acct !== account?.username;
|
const isRemote = account?.acct !== account?.username;
|
||||||
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
|
const remoteDomain = isRemote ? account?.acct.split('@')[1] : null;
|
||||||
@ -808,11 +778,9 @@ export const AccountHeader: React.FC<{
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames('account__header', {
|
className={classNames('account__header animate-parent', {
|
||||||
inactive: !!account.moved,
|
inactive: !!account.moved,
|
||||||
})}
|
})}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
>
|
||||||
{!(suspended || hidden || account.moved) &&
|
{!(suspended || hidden || account.moved) &&
|
||||||
relationship?.requested_by && (
|
relationship?.requested_by && (
|
||||||
|
|||||||
@ -12,14 +12,12 @@ import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
|
|||||||
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
|
||||||
import { Icon } from '@/mastodon/components/icon';
|
import { Icon } from '@/mastodon/components/icon';
|
||||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||||
import { isFeatureEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
|
||||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||||
|
|
||||||
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
|
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
|
||||||
|
|
||||||
import { messages as privacyMessages } from './privacy_dropdown';
|
import { messages as privacyMessages } from './privacy_dropdown';
|
||||||
|
|
||||||
@ -43,9 +41,6 @@ interface PrivacyDropdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
|
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
|
||||||
if (!isFeatureEnabled('outgoing_quotes')) {
|
|
||||||
return <PrivacyDropdownContainer {...props} />;
|
|
||||||
}
|
|
||||||
return <PrivacyModalButton {...props} />;
|
return <PrivacyModalButton {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,6 @@ import { IconButton } from 'mastodon/components/icon_button';
|
|||||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import StatusContent from 'mastodon/components/status_content';
|
import StatusContent from 'mastodon/components/status_content';
|
||||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||||
import { autoPlayGif } from 'mastodon/initial_state';
|
|
||||||
import { makeGetStatus } from 'mastodon/selectors';
|
import { makeGetStatus } from 'mastodon/selectors';
|
||||||
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
import { LinkedDisplayName } from '@/mastodon/components/display_name';
|
||||||
|
|
||||||
@ -57,32 +56,6 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
|||||||
const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId }));
|
const lastStatus = useSelector(state => getStatus(state, { id: lastStatusId }));
|
||||||
const accounts = useSelector(state => getAccounts(state, accountIds));
|
const accounts = useSelector(state => getAccounts(state, accountIds));
|
||||||
|
|
||||||
const handleMouseEnter = useCallback(({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-original');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-static');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
if (unread) {
|
if (unread) {
|
||||||
dispatch(markConversationRead(id));
|
dispatch(markConversationRead(id));
|
||||||
@ -163,7 +136,7 @@ export const Conversation = ({ conversation, scrollKey }) => {
|
|||||||
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
{unread && <span className='conversation__unread' />} <RelativeTimestamp timestamp={lastStatus.get('created_at')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='conversation__content__names' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
<div className='conversation__content__names animate-parent'>
|
||||||
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
<FormattedMessage id='conversation.with' defaultMessage='With {names}' values={{ names: <span>{names}</span> }} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { MouseEventHandler } from 'react';
|
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
@ -44,39 +43,6 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
|
|||||||
const account = useAppSelector((s) => getAccount(s, accountId));
|
const account = useAppSelector((s) => getAccount(s, accountId));
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleMouseEnter = useCallback<MouseEventHandler>(
|
|
||||||
({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
|
||||||
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
const original = emoji.getAttribute('data-original');
|
|
||||||
if (original) emoji.src = original;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback<MouseEventHandler>(
|
|
||||||
({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
|
||||||
|
|
||||||
emojis.forEach((emoji) => {
|
|
||||||
const staticUrl = emoji.getAttribute('data-static');
|
|
||||||
if (staticUrl) emoji.src = staticUrl;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFollow = useCallback(() => {
|
const handleFollow = useCallback(() => {
|
||||||
if (!account) return;
|
if (!account) return;
|
||||||
|
|
||||||
@ -185,9 +151,7 @@ export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
|
|||||||
|
|
||||||
{account.get('note').length > 0 && (
|
{account.get('note').length > 0 && (
|
||||||
<div
|
<div
|
||||||
className='account-card__bio translate'
|
className='account-card__bio translate animate-parent'
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import type { ComponentPropsWithoutRef, ElementType } from 'react';
|
import type { ComponentPropsWithoutRef, ElementType } from 'react';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
import { isModernEmojiEnabled } from '@/mastodon/utils/environment';
|
||||||
|
|
||||||
import { useEmojify } from './hooks';
|
import { useEmojify } from './hooks';
|
||||||
@ -7,12 +9,13 @@ import type { CustomEmojiMapArg } from './types';
|
|||||||
|
|
||||||
type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
|
type EmojiHTMLProps<Element extends ElementType = 'div'> = Omit<
|
||||||
ComponentPropsWithoutRef<Element>,
|
ComponentPropsWithoutRef<Element>,
|
||||||
'dangerouslySetInnerHTML'
|
'dangerouslySetInnerHTML' | 'className'
|
||||||
> & {
|
> & {
|
||||||
htmlString: string;
|
htmlString: string;
|
||||||
extraEmojis?: CustomEmojiMapArg;
|
extraEmojis?: CustomEmojiMapArg;
|
||||||
as?: Element;
|
as?: Element;
|
||||||
shallow?: boolean;
|
shallow?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModernEmojiHTML = ({
|
export const ModernEmojiHTML = ({
|
||||||
@ -20,6 +23,7 @@ export const ModernEmojiHTML = ({
|
|||||||
htmlString,
|
htmlString,
|
||||||
as: Wrapper = 'div', // Rename for syntax highlighting
|
as: Wrapper = 'div', // Rename for syntax highlighting
|
||||||
shallow,
|
shallow,
|
||||||
|
className = '',
|
||||||
...props
|
...props
|
||||||
}: EmojiHTMLProps<ElementType>) => {
|
}: EmojiHTMLProps<ElementType>) => {
|
||||||
const emojifiedHtml = useEmojify({
|
const emojifiedHtml = useEmojify({
|
||||||
@ -33,7 +37,11 @@ export const ModernEmojiHTML = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper {...props} dangerouslySetInnerHTML={{ __html: emojifiedHtml }} />
|
<Wrapper
|
||||||
|
{...props}
|
||||||
|
className={classNames(className, 'animate-parent')}
|
||||||
|
dangerouslySetInnerHTML={{ __html: emojifiedHtml }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,7 +51,13 @@ export const EmojiHTML = <Element extends ElementType>(
|
|||||||
if (isModernEmojiEnabled()) {
|
if (isModernEmojiEnabled()) {
|
||||||
return <ModernEmojiHTML {...props} />;
|
return <ModernEmojiHTML {...props} />;
|
||||||
}
|
}
|
||||||
const { as: asElement, htmlString, extraEmojis, ...rest } = props;
|
const { as: asElement, htmlString, extraEmojis, className, ...rest } = props;
|
||||||
const Wrapper = asElement ?? 'div';
|
const Wrapper = asElement ?? 'div';
|
||||||
return <Wrapper {...rest} dangerouslySetInnerHTML={{ __html: htmlString }} />;
|
return (
|
||||||
|
<Wrapper
|
||||||
|
{...rest}
|
||||||
|
dangerouslySetInnerHTML={{ __html: htmlString }}
|
||||||
|
className={classNames(className, 'animate-parent')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
61
app/javascript/mastodon/features/emoji/handlers.ts
Normal file
61
app/javascript/mastodon/features/emoji/handlers.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { autoPlayGif } from '@/mastodon/initial_state';
|
||||||
|
|
||||||
|
const PARENT_MAX_DEPTH = 10;
|
||||||
|
|
||||||
|
export function handleAnimateGif(event: MouseEvent) {
|
||||||
|
// We already check this in ui/index.jsx, but just to be sure.
|
||||||
|
if (autoPlayGif) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { target, type } = event;
|
||||||
|
const animate = type === 'mouseover'; // Mouse over = animate, mouse out = don't animate.
|
||||||
|
|
||||||
|
if (target instanceof HTMLImageElement) {
|
||||||
|
setAnimateGif(target, animate);
|
||||||
|
} else if (!(target instanceof HTMLElement) || target === document.body) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parent: HTMLElement | null = null;
|
||||||
|
let iter = 0;
|
||||||
|
|
||||||
|
if (target.classList.contains('animate-parent')) {
|
||||||
|
parent = target;
|
||||||
|
} else {
|
||||||
|
// Iterate up to PARENT_MAX_DEPTH levels up the DOM tree to find a parent with the class 'animate-parent'.
|
||||||
|
let current: HTMLElement | null = target;
|
||||||
|
while (current) {
|
||||||
|
if (iter >= PARENT_MAX_DEPTH) {
|
||||||
|
return; // We can just exit right now.
|
||||||
|
}
|
||||||
|
current = current.parentElement;
|
||||||
|
if (current?.classList.contains('animate-parent')) {
|
||||||
|
parent = current;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
iter++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Affect all animated children within the parent.
|
||||||
|
if (parent) {
|
||||||
|
const animatedChildren =
|
||||||
|
parent.querySelectorAll<HTMLImageElement>('img.custom-emoji');
|
||||||
|
for (const child of animatedChildren) {
|
||||||
|
setAnimateGif(child, animate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAnimateGif(image: HTMLImageElement, animate: boolean) {
|
||||||
|
const { classList, dataset } = image;
|
||||||
|
if (
|
||||||
|
!classList.contains('custom-emoji') ||
|
||||||
|
!dataset.static ||
|
||||||
|
!dataset.original
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
image.src = animate ? dataset.original : dataset.static;
|
||||||
|
}
|
||||||
@ -111,42 +111,14 @@ class ContentWithRouter extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
handleMouseEnter = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-original');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleMouseLeave = ({ currentTarget }) => {
|
|
||||||
if (autoPlayGif) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emojis = currentTarget.querySelectorAll('.custom-emoji');
|
|
||||||
|
|
||||||
for (var i = 0; i < emojis.length; i++) {
|
|
||||||
let emoji = emojis[i];
|
|
||||||
emoji.src = emoji.getAttribute('data-static');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { announcement } = this.props;
|
const { announcement } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='announcements__item__content translate'
|
className='announcements__item__content translate animate-parent'
|
||||||
ref={this.setRef}
|
ref={this.setRef}
|
||||||
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
|
dangerouslySetInnerHTML={{ __html: announcement.get('contentHtml') }}
|
||||||
onMouseEnter={this.handleMouseEnter}
|
|
||||||
onMouseLeave={this.handleMouseLeave}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -238,9 +210,21 @@ class Reaction extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
|
<animated.button
|
||||||
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
|
className={classNames('reactions-bar__item', { active: reaction.get('me') })}
|
||||||
<span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
|
onClick={this.handleClick}
|
||||||
|
title={`:${shortCode}:`}
|
||||||
|
style={this.props.style}
|
||||||
|
// This does not use animate-parent as this component is directly rendered by React.
|
||||||
|
onMouseEnter={this.handleMouseEnter}
|
||||||
|
onMouseLeave={this.handleMouseLeave}
|
||||||
|
>
|
||||||
|
<span className='reactions-bar__item__emoji'>
|
||||||
|
<Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} />
|
||||||
|
</span>
|
||||||
|
<span className='reactions-bar__item__count'>
|
||||||
|
<AnimatedNumber value={reaction.get('count')} />
|
||||||
|
</span>
|
||||||
</animated.button>
|
</animated.button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||||||
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
|
||||||
import Column from 'mastodon/components/column';
|
import Column from 'mastodon/components/column';
|
||||||
import ColumnHeader from 'mastodon/components/column_header';
|
import ColumnHeader from 'mastodon/components/column_header';
|
||||||
import { isFeatureEnabled } from 'mastodon/utils/environment';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
|
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
|
||||||
@ -63,12 +62,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
|
|||||||
<td><kbd>b</kbd></td>
|
<td><kbd>b</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
|
||||||
</tr>
|
</tr>
|
||||||
{isFeatureEnabled('outgoing_quotes') && (
|
<tr>
|
||||||
<tr>
|
<td><kbd>q</kbd></td>
|
||||||
<td><kbd>q</kbd></td>
|
<td><FormattedMessage id='keyboard_shortcuts.quote' defaultMessage='Quote post' /></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.quote' defaultMessage='Quote post' /></td>
|
</tr>
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
<tr>
|
<tr>
|
||||||
<td><kbd>enter</kbd>, <kbd>o</kbd></td>
|
<td><kbd>enter</kbd>, <kbd>o</kbd></td>
|
||||||
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
|
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>
|
||||||
|
|||||||
@ -76,32 +76,6 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
|||||||
[clickCoordinatesRef, statusId, account, history],
|
[clickCoordinatesRef, statusId, account, history],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseEnter = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
|
||||||
({ currentTarget }) => {
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
const newSrc = emoji.getAttribute('data-original');
|
|
||||||
if (newSrc) emoji.src = newSrc;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMouseLeave = useCallback<React.MouseEventHandler<HTMLDivElement>>(
|
|
||||||
({ currentTarget }) => {
|
|
||||||
const emojis =
|
|
||||||
currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
|
|
||||||
|
|
||||||
for (const emoji of emojis) {
|
|
||||||
const newSrc = emoji.getAttribute('data-static');
|
|
||||||
if (newSrc) emoji.src = newSrc;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleContentWarningClick = useCallback(() => {
|
const handleContentWarningClick = useCallback(() => {
|
||||||
dispatch(toggleStatusSpoilers(statusId));
|
dispatch(toggleStatusSpoilers(statusId));
|
||||||
}, [dispatch, statusId]);
|
}, [dispatch, statusId]);
|
||||||
@ -123,13 +97,11 @@ export const EmbeddedStatus: React.FC<{ statusId: string }> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='notification-group__embedded-status'
|
className='notification-group__embedded-status animate-parent'
|
||||||
role='button'
|
role='button'
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
onMouseUp={handleMouseUp}
|
onMouseUp={handleMouseUp}
|
||||||
onMouseEnter={handleMouseEnter}
|
|
||||||
onMouseLeave={handleMouseLeave}
|
|
||||||
>
|
>
|
||||||
<div className='notification-group__embedded-status__account'>
|
<div className='notification-group__embedded-status__account'>
|
||||||
<Avatar account={account} size={16} />
|
<Avatar account={account} size={16} />
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
|
|||||||
import { IconButton } from '../../../components/icon_button';
|
import { IconButton } from '../../../components/icon_button';
|
||||||
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
import { Dropdown } from 'mastodon/components/dropdown_menu';
|
||||||
import { me } from '../../../initial_state';
|
import { me } from '../../../initial_state';
|
||||||
import { isFeatureEnabled } from '@/mastodon/utils/environment';
|
|
||||||
import { BoostButton } from '@/mastodon/components/status/boost_button';
|
import { BoostButton } from '@/mastodon/components/status/boost_button';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
@ -237,7 +236,7 @@ class ActionBar extends PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
|
if (!['private', 'direct'].includes(status.get('visibility'))) {
|
||||||
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
|
||||||
}
|
}
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useEffect, useState, useCallback } from 'react';
|
||||||
|
|
||||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
fetchContext,
|
fetchContext,
|
||||||
@ -8,31 +8,80 @@ import {
|
|||||||
} from 'mastodon/actions/statuses';
|
} from 'mastodon/actions/statuses';
|
||||||
import type { AsyncRefreshHeader } from 'mastodon/api';
|
import type { AsyncRefreshHeader } from 'mastodon/api';
|
||||||
import { apiGetAsyncRefresh } from 'mastodon/api/async_refreshes';
|
import { apiGetAsyncRefresh } from 'mastodon/api/async_refreshes';
|
||||||
|
import { Alert } from 'mastodon/components/alert';
|
||||||
|
import { ExitAnimationWrapper } from 'mastodon/components/exit_animation_wrapper';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
const AnimatedAlert: React.FC<
|
||||||
|
React.ComponentPropsWithoutRef<typeof Alert> & { withEntryDelay?: boolean }
|
||||||
|
> = ({ isActive = false, withEntryDelay, ...props }) => (
|
||||||
|
<ExitAnimationWrapper withEntryDelay isActive={isActive}>
|
||||||
|
{(delayedIsActive) => <Alert isActive={delayedIsActive} {...props} />}
|
||||||
|
</ExitAnimationWrapper>
|
||||||
|
);
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
loading: {
|
moreFound: {
|
||||||
|
id: 'status.context.more_replies_found',
|
||||||
|
defaultMessage: 'More replies found',
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
id: 'status.context.show',
|
||||||
|
defaultMessage: 'Show',
|
||||||
|
},
|
||||||
|
loadingInitial: {
|
||||||
id: 'status.context.loading',
|
id: 'status.context.loading',
|
||||||
defaultMessage: 'Checking for more replies',
|
defaultMessage: 'Loading',
|
||||||
|
},
|
||||||
|
loadingMore: {
|
||||||
|
id: 'status.context.loading_more',
|
||||||
|
defaultMessage: 'Loading more replies',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
id: 'status.context.loading_success',
|
||||||
|
defaultMessage: 'All replies loaded',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
id: 'status.context.loading_error',
|
||||||
|
defaultMessage: "Couldn't load new replies",
|
||||||
|
},
|
||||||
|
retry: {
|
||||||
|
id: 'status.context.retry',
|
||||||
|
defaultMessage: 'Retry',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type LoadingState =
|
||||||
|
| 'idle'
|
||||||
|
| 'more-available'
|
||||||
|
| 'loading-initial'
|
||||||
|
| 'loading-more'
|
||||||
|
| 'success'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
export const RefreshController: React.FC<{
|
export const RefreshController: React.FC<{
|
||||||
statusId: string;
|
statusId: string;
|
||||||
}> = ({ statusId }) => {
|
}> = ({ statusId }) => {
|
||||||
const refresh = useAppSelector(
|
const refresh = useAppSelector(
|
||||||
(state) => state.contexts.refreshing[statusId],
|
(state) => state.contexts.refreshing[statusId],
|
||||||
);
|
);
|
||||||
const autoRefresh = useAppSelector(
|
const currentReplyCount = useAppSelector(
|
||||||
(state) =>
|
(state) => state.contexts.replies[statusId]?.length ?? 0,
|
||||||
!state.contexts.replies[statusId] ||
|
|
||||||
state.contexts.replies[statusId].length === 0,
|
|
||||||
);
|
);
|
||||||
|
const autoRefresh = !currentReplyCount;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const [ready, setReady] = useState(false);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loadingState, setLoadingState] = useState<LoadingState>(
|
||||||
|
refresh && autoRefresh ? 'loading-initial' : 'idle',
|
||||||
|
);
|
||||||
|
|
||||||
|
const [wasDismissed, setWasDismissed] = useState(false);
|
||||||
|
const dismissPrompt = useCallback(() => {
|
||||||
|
setWasDismissed(true);
|
||||||
|
setLoadingState('idle');
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeoutId: ReturnType<typeof setTimeout>;
|
let timeoutId: ReturnType<typeof setTimeout>;
|
||||||
@ -45,67 +94,104 @@ export const RefreshController: React.FC<{
|
|||||||
|
|
||||||
if (result.async_refresh.result_count > 0) {
|
if (result.async_refresh.result_count > 0) {
|
||||||
if (autoRefresh) {
|
if (autoRefresh) {
|
||||||
void dispatch(fetchContext({ statusId }));
|
void dispatch(fetchContext({ statusId })).then(() => {
|
||||||
return '';
|
setLoadingState('idle');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setLoadingState('more-available');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
setReady(true);
|
setLoadingState('idle');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scheduleRefresh(refresh);
|
scheduleRefresh(refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
return '';
|
|
||||||
});
|
});
|
||||||
}, refresh.retry * 1000);
|
}, refresh.retry * 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (refresh) {
|
if (refresh && !wasDismissed) {
|
||||||
scheduleRefresh(refresh);
|
scheduleRefresh(refresh);
|
||||||
|
setLoadingState('loading-initial');
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearTimeout(timeoutId);
|
clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [dispatch, setReady, statusId, refresh, autoRefresh]);
|
}, [dispatch, statusId, refresh, autoRefresh, wasDismissed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Hide success message after a short delay
|
||||||
|
if (loadingState === 'success') {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
setLoadingState('idle');
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => '';
|
||||||
|
}, [loadingState]);
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
setLoading(true);
|
setLoadingState('loading-more');
|
||||||
setReady(false);
|
|
||||||
|
|
||||||
dispatch(fetchContext({ statusId }))
|
dispatch(fetchContext({ statusId }))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setLoading(false);
|
setLoadingState('success');
|
||||||
return '';
|
return '';
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setLoading(false);
|
setLoadingState('error');
|
||||||
});
|
});
|
||||||
}, [dispatch, setReady, statusId]);
|
}, [dispatch, statusId]);
|
||||||
|
|
||||||
if (ready && !loading) {
|
if (loadingState === 'loading-initial') {
|
||||||
return (
|
return (
|
||||||
<button className='load-more load-gap' onClick={handleClick}>
|
<div
|
||||||
<FormattedMessage
|
className='load-more load-gap'
|
||||||
id='status.context.load_new_replies'
|
aria-busy
|
||||||
defaultMessage='New replies available'
|
aria-live='polite'
|
||||||
/>
|
aria-label={intl.formatMessage(messages.loadingInitial)}
|
||||||
</button>
|
>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!refresh && !loading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className='column__alert' role='status' aria-live='polite'>
|
||||||
className='load-more load-gap'
|
<AnimatedAlert
|
||||||
aria-busy
|
isActive={loadingState === 'more-available'}
|
||||||
aria-live='polite'
|
message={intl.formatMessage(messages.moreFound)}
|
||||||
aria-label={intl.formatMessage(messages.loading)}
|
action={intl.formatMessage(messages.show)}
|
||||||
>
|
onActionClick={handleClick}
|
||||||
<LoadingIndicator />
|
onDismiss={dismissPrompt}
|
||||||
|
animateFrom='below'
|
||||||
|
/>
|
||||||
|
<AnimatedAlert
|
||||||
|
isLoading
|
||||||
|
withEntryDelay
|
||||||
|
isActive={loadingState === 'loading-more'}
|
||||||
|
message={intl.formatMessage(messages.loadingMore)}
|
||||||
|
animateFrom='below'
|
||||||
|
/>
|
||||||
|
<AnimatedAlert
|
||||||
|
withEntryDelay
|
||||||
|
isActive={loadingState === 'error'}
|
||||||
|
message={intl.formatMessage(messages.error)}
|
||||||
|
action={intl.formatMessage(messages.retry)}
|
||||||
|
onActionClick={handleClick}
|
||||||
|
onDismiss={dismissPrompt}
|
||||||
|
animateFrom='below'
|
||||||
|
/>
|
||||||
|
<AnimatedAlert
|
||||||
|
withEntryDelay
|
||||||
|
isActive={loadingState === 'success'}
|
||||||
|
message={intl.formatMessage(messages.success)}
|
||||||
|
animateFrom='below'
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
|
import { difference } from 'lodash';
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
@ -150,6 +151,11 @@ class Status extends ImmutablePureComponent {
|
|||||||
fullscreen: false,
|
fullscreen: false,
|
||||||
showMedia: defaultMediaVisibility(this.props.status),
|
showMedia: defaultMediaVisibility(this.props.status),
|
||||||
loadedStatusId: undefined,
|
loadedStatusId: undefined,
|
||||||
|
/**
|
||||||
|
* Holds the ids of newly added replies, excluding the initial load.
|
||||||
|
* Used to highlight newly added replies in the UI
|
||||||
|
*/
|
||||||
|
newRepliesIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
UNSAFE_componentWillMount () {
|
||||||
@ -462,6 +468,7 @@ class Status extends ImmutablePureComponent {
|
|||||||
previousId={i > 0 ? list[i - 1] : undefined}
|
previousId={i > 0 ? list[i - 1] : undefined}
|
||||||
nextId={list[i + 1] || (ancestors && statusId)}
|
nextId={list[i + 1] || (ancestors && statusId)}
|
||||||
rootId={statusId}
|
rootId={statusId}
|
||||||
|
shouldHighlightOnMount={this.state.newRepliesIds.includes(id)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -495,11 +502,20 @@ class Status extends ImmutablePureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
const { status, ancestorsIds } = this.props;
|
const { status, ancestorsIds, descendantsIds } = this.props;
|
||||||
|
|
||||||
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) {
|
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || prevProps.status?.get('id') !== status.get('id'))) {
|
||||||
this._scrollStatusIntoView();
|
this._scrollStatusIntoView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only highlight replies after the initial load
|
||||||
|
if (prevProps.descendantsIds.length) {
|
||||||
|
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
|
||||||
|
|
||||||
|
if (newRepliesIds.length) {
|
||||||
|
this.setState({newRepliesIds});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
@ -632,8 +648,8 @@ class Status extends ImmutablePureComponent {
|
|||||||
</div>
|
</div>
|
||||||
</Hotkeys>
|
</Hotkeys>
|
||||||
|
|
||||||
{remoteHint}
|
|
||||||
{descendants}
|
{descendants}
|
||||||
|
{remoteHint}
|
||||||
</div>
|
</div>
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
|
|
||||||
|
|||||||
@ -22,11 +22,12 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
|
|||||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||||
|
|
||||||
|
import { handleAnimateGif } from '../emoji/handlers';
|
||||||
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
|
||||||
import { clearHeight } from '../../actions/height_cache';
|
import { clearHeight } from '../../actions/height_cache';
|
||||||
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
|
||||||
import { expandHomeTimeline } from '../../actions/timelines';
|
import { expandHomeTimeline } from '../../actions/timelines';
|
||||||
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards } from '../../initial_state';
|
import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding, disableHoverCards, autoPlayGif } from '../../initial_state';
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import { NavigationBar } from './components/navigation_bar';
|
import { NavigationBar } from './components/navigation_bar';
|
||||||
@ -379,6 +380,11 @@ class UI extends PureComponent {
|
|||||||
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
window.addEventListener('beforeunload', this.handleBeforeUnload, false);
|
||||||
window.addEventListener('resize', this.handleResize, { passive: true });
|
window.addEventListener('resize', this.handleResize, { passive: true });
|
||||||
|
|
||||||
|
if (!autoPlayGif) {
|
||||||
|
window.addEventListener('mouseover', handleAnimateGif, { passive: true });
|
||||||
|
window.addEventListener('mouseout', handleAnimateGif, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('dragenter', this.handleDragEnter, false);
|
document.addEventListener('dragenter', this.handleDragEnter, false);
|
||||||
document.addEventListener('dragover', this.handleDragOver, false);
|
document.addEventListener('dragover', this.handleDragOver, false);
|
||||||
document.addEventListener('drop', this.handleDrop, false);
|
document.addEventListener('drop', this.handleDrop, false);
|
||||||
@ -404,6 +410,8 @@ class UI extends PureComponent {
|
|||||||
window.removeEventListener('blur', this.handleWindowBlur);
|
window.removeEventListener('blur', this.handleWindowBlur);
|
||||||
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
window.removeEventListener('beforeunload', this.handleBeforeUnload);
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
|
window.removeEventListener('mouseover', handleAnimateGif);
|
||||||
|
window.removeEventListener('mouseout', handleAnimateGif);
|
||||||
|
|
||||||
document.removeEventListener('dragenter', this.handleDragEnter);
|
document.removeEventListener('dragenter', this.handleDragEnter);
|
||||||
document.removeEventListener('dragover', this.handleDragOver);
|
document.removeEventListener('dragover', this.handleDragOver);
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Zrušit boostnutí",
|
"status.cancel_reblog_private": "Zrušit boostnutí",
|
||||||
"status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek",
|
"status.cannot_quote": "Nemáte oprávnění citovat tento příspěvek",
|
||||||
"status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
|
"status.cannot_reblog": "Tento příspěvek nemůže být boostnutý",
|
||||||
|
"status.contains_quote": "Obsahuje citaci",
|
||||||
"status.context.load_new_replies": "K dispozici jsou nové odpovědi",
|
"status.context.load_new_replies": "K dispozici jsou nové odpovědi",
|
||||||
"status.context.loading": "Hledání dalších odpovědí",
|
"status.context.loading": "Hledání dalších odpovědí",
|
||||||
"status.continued_thread": "Pokračuje ve vlákně",
|
"status.continued_thread": "Pokračuje ve vlákně",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Příspěvek odstraněn autorem",
|
"status.quote_error.revoked": "Příspěvek odstraněn autorem",
|
||||||
"status.quote_followers_only": "Pouze moji sledující mohou citovat tento příspěvek",
|
"status.quote_followers_only": "Pouze moji sledující mohou citovat tento příspěvek",
|
||||||
"status.quote_manual_review": "Autor provede manuální kontrolu",
|
"status.quote_manual_review": "Autor provede manuální kontrolu",
|
||||||
|
"status.quote_noun": "Citace",
|
||||||
"status.quote_policy_change": "Změňte, kdo může citovat",
|
"status.quote_policy_change": "Změňte, kdo může citovat",
|
||||||
"status.quote_post_author": "Citovali příspěvek od @{name}",
|
"status.quote_post_author": "Citovali příspěvek od @{name}",
|
||||||
"status.quote_private": "Soukromé příspěvky nelze citovat",
|
"status.quote_private": "Soukromé příspěvky nelze citovat",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Dadhybu",
|
"status.cancel_reblog_private": "Dadhybu",
|
||||||
"status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn",
|
"status.cannot_quote": "Does dim caniatâd i chi ddyfynnu'r postiad hwn",
|
||||||
"status.cannot_reblog": "Does dim modd hybu'r postiad hwn",
|
"status.cannot_reblog": "Does dim modd hybu'r postiad hwn",
|
||||||
|
"status.contains_quote": "Yn cynnwys dyfyniad",
|
||||||
"status.context.load_new_replies": "Mae atebion newydd ar gael",
|
"status.context.load_new_replies": "Mae atebion newydd ar gael",
|
||||||
"status.context.loading": "Yn chwilio am fwy o atebion",
|
"status.context.loading": "Yn chwilio am fwy o atebion",
|
||||||
"status.continued_thread": "Edefyn parhaus",
|
"status.continued_thread": "Edefyn parhaus",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Postiad wedi'i ddileu gan yr awdur",
|
"status.quote_error.revoked": "Postiad wedi'i ddileu gan yr awdur",
|
||||||
"status.quote_followers_only": "Dim ond dilynwyr all ddyfynnu'r postiad hwn",
|
"status.quote_followers_only": "Dim ond dilynwyr all ddyfynnu'r postiad hwn",
|
||||||
"status.quote_manual_review": "Bydd yr awdur yn ei adolygu ei hyn",
|
"status.quote_manual_review": "Bydd yr awdur yn ei adolygu ei hyn",
|
||||||
|
"status.quote_noun": "Dyfynnu",
|
||||||
"status.quote_policy_change": "Newid pwy all ddyfynnu",
|
"status.quote_policy_change": "Newid pwy all ddyfynnu",
|
||||||
"status.quote_post_author": "Wedi dyfynnu postiad gan @{name}",
|
"status.quote_post_author": "Wedi dyfynnu postiad gan @{name}",
|
||||||
"status.quote_private": "Does dim modd dyfynnu postiadau preifat",
|
"status.quote_private": "Does dim modd dyfynnu postiadau preifat",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Fjern fremhævelse",
|
"status.cancel_reblog_private": "Fjern fremhævelse",
|
||||||
"status.cannot_quote": "Du har ikke tilladelse til at citere dette indlæg",
|
"status.cannot_quote": "Du har ikke tilladelse til at citere dette indlæg",
|
||||||
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
|
"status.cannot_reblog": "Dette indlæg kan ikke fremhæves",
|
||||||
|
"status.contains_quote": "Indeholder citat",
|
||||||
"status.context.load_new_replies": "Nye svar tilgængelige",
|
"status.context.load_new_replies": "Nye svar tilgængelige",
|
||||||
"status.context.loading": "Tjekker for flere svar",
|
"status.context.loading": "Tjekker for flere svar",
|
||||||
"status.continued_thread": "Fortsat tråd",
|
"status.continued_thread": "Fortsat tråd",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Indlæg fjernet af forfatter",
|
"status.quote_error.revoked": "Indlæg fjernet af forfatter",
|
||||||
"status.quote_followers_only": "Kun følgere kan citere dette indlæg",
|
"status.quote_followers_only": "Kun følgere kan citere dette indlæg",
|
||||||
"status.quote_manual_review": "Forfatter vil manuelt gennemgå",
|
"status.quote_manual_review": "Forfatter vil manuelt gennemgå",
|
||||||
|
"status.quote_noun": "Citat",
|
||||||
"status.quote_policy_change": "Ændr hvem der kan citere",
|
"status.quote_policy_change": "Ændr hvem der kan citere",
|
||||||
"status.quote_post_author": "Citerede et indlæg fra @{name}",
|
"status.quote_post_author": "Citerede et indlæg fra @{name}",
|
||||||
"status.quote_private": "Private indlæg kan ikke citeres",
|
"status.quote_private": "Private indlæg kan ikke citeres",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
|
"status.cancel_reblog_private": "Beitrag nicht mehr teilen",
|
||||||
"status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren",
|
"status.cannot_quote": "Dir ist es nicht gestattet, diesen Beitrag zu zitieren",
|
||||||
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
|
"status.cannot_reblog": "Dieser Beitrag kann nicht geteilt werden",
|
||||||
|
"status.contains_quote": "Enthält Zitat",
|
||||||
"status.context.load_new_replies": "Neue Antworten verfügbar",
|
"status.context.load_new_replies": "Neue Antworten verfügbar",
|
||||||
"status.context.loading": "Weitere Antworten werden abgerufen",
|
"status.context.loading": "Weitere Antworten werden abgerufen",
|
||||||
"status.continued_thread": "Fortgeführter Thread",
|
"status.continued_thread": "Fortgeführter Thread",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Beitrag durch Autor*in entfernt",
|
"status.quote_error.revoked": "Beitrag durch Autor*in entfernt",
|
||||||
"status.quote_followers_only": "Nur Follower können diesen Beitrag zitieren",
|
"status.quote_followers_only": "Nur Follower können diesen Beitrag zitieren",
|
||||||
"status.quote_manual_review": "Zitierte*r überprüft manuell",
|
"status.quote_manual_review": "Zitierte*r überprüft manuell",
|
||||||
|
"status.quote_noun": "Zitat",
|
||||||
"status.quote_policy_change": "Ändern, wer zitieren darf",
|
"status.quote_policy_change": "Ändern, wer zitieren darf",
|
||||||
"status.quote_post_author": "Zitierte einen Beitrag von @{name}",
|
"status.quote_post_author": "Zitierte einen Beitrag von @{name}",
|
||||||
"status.quote_private": "Private Beiträge können nicht zitiert werden",
|
"status.quote_private": "Private Beiträge können nicht zitiert werden",
|
||||||
|
|||||||
@ -736,7 +736,7 @@
|
|||||||
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
|
"privacy.private.long": "Μόνο οι ακόλουθοί σας",
|
||||||
"privacy.private.short": "Ακόλουθοι",
|
"privacy.private.short": "Ακόλουθοι",
|
||||||
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
|
"privacy.public.long": "Όλοι εντός και εκτός του Mastodon",
|
||||||
"privacy.public.short": "Δημόσιο",
|
"privacy.public.short": "Δημόσια",
|
||||||
"privacy.quote.anyone": "{visibility}, ο καθένας μπορεί να παραθέσει",
|
"privacy.quote.anyone": "{visibility}, ο καθένας μπορεί να παραθέσει",
|
||||||
"privacy.quote.disabled": "{visibility}, παραθέσεις απενεργοποιημένες",
|
"privacy.quote.disabled": "{visibility}, παραθέσεις απενεργοποιημένες",
|
||||||
"privacy.quote.limited": "{visibility}, παραθέσεις περιορισμένες",
|
"privacy.quote.limited": "{visibility}, παραθέσεις περιορισμένες",
|
||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
|
"status.cancel_reblog_private": "Ακύρωση ενίσχυσης",
|
||||||
"status.cannot_quote": "Δε σας επιτρέπετε να παραθέσετε αυτή την ανάρτηση",
|
"status.cannot_quote": "Δε σας επιτρέπετε να παραθέσετε αυτή την ανάρτηση",
|
||||||
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
|
"status.cannot_reblog": "Αυτή η ανάρτηση δεν μπορεί να ενισχυθεί",
|
||||||
|
"status.contains_quote": "Περιέχει παράθεση",
|
||||||
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
|
"status.context.load_new_replies": "Νέες απαντήσεις διαθέσιμες",
|
||||||
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
|
"status.context.loading": "Γίνεται έλεγχος για περισσότερες απαντήσεις",
|
||||||
"status.continued_thread": "Συνεχιζόμενο νήματος",
|
"status.continued_thread": "Συνεχιζόμενο νήματος",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Η ανάρτηση αφαιρέθηκε από τον συντάκτη",
|
"status.quote_error.revoked": "Η ανάρτηση αφαιρέθηκε από τον συντάκτη",
|
||||||
"status.quote_followers_only": "Μόνο οι ακόλουθοι μπορούν να παραθέσουν αυτή την ανάρτηση",
|
"status.quote_followers_only": "Μόνο οι ακόλουθοι μπορούν να παραθέσουν αυτή την ανάρτηση",
|
||||||
"status.quote_manual_review": "Ο συντάκτης θα επανεξετάσει χειροκίνητα",
|
"status.quote_manual_review": "Ο συντάκτης θα επανεξετάσει χειροκίνητα",
|
||||||
|
"status.quote_noun": "Παράθεση",
|
||||||
"status.quote_policy_change": "Αλλάξτε ποιός μπορεί να κάνει παράθεση",
|
"status.quote_policy_change": "Αλλάξτε ποιός μπορεί να κάνει παράθεση",
|
||||||
"status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}",
|
"status.quote_post_author": "Παρατίθεται μια ανάρτηση από @{name}",
|
||||||
"status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν",
|
"status.quote_private": "Ιδιωτικές αναρτήσεις δεν μπορούν να παρατεθούν",
|
||||||
|
|||||||
@ -865,8 +865,13 @@
|
|||||||
"status.cannot_quote": "You are not allowed to quote this post",
|
"status.cannot_quote": "You are not allowed to quote this post",
|
||||||
"status.cannot_reblog": "This post cannot be boosted",
|
"status.cannot_reblog": "This post cannot be boosted",
|
||||||
"status.contains_quote": "Contains quote",
|
"status.contains_quote": "Contains quote",
|
||||||
"status.context.load_new_replies": "New replies available",
|
"status.context.loading": "Loading more replies",
|
||||||
"status.context.loading": "Checking for more replies",
|
"status.context.loading_error": "Couldn't load new replies",
|
||||||
|
"status.context.loading_more": "Loading more replies",
|
||||||
|
"status.context.loading_success": "All replies loaded",
|
||||||
|
"status.context.more_replies_found": "More replies found",
|
||||||
|
"status.context.retry": "Retry",
|
||||||
|
"status.context.show": "Show",
|
||||||
"status.continued_thread": "Continued thread",
|
"status.continued_thread": "Continued thread",
|
||||||
"status.copy": "Copy link to post",
|
"status.copy": "Copy link to post",
|
||||||
"status.delete": "Delete",
|
"status.delete": "Delete",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Quitar adhesión",
|
"status.cancel_reblog_private": "Quitar adhesión",
|
||||||
"status.cannot_quote": "No te es permitido citar este mensaje",
|
"status.cannot_quote": "No te es permitido citar este mensaje",
|
||||||
"status.cannot_reblog": "No se puede adherir a este mensaje",
|
"status.cannot_reblog": "No se puede adherir a este mensaje",
|
||||||
|
"status.contains_quote": "Contiene cita",
|
||||||
"status.context.load_new_replies": "Hay nuevas respuestas",
|
"status.context.load_new_replies": "Hay nuevas respuestas",
|
||||||
"status.context.loading": "Buscando más respuestas",
|
"status.context.loading": "Buscando más respuestas",
|
||||||
"status.continued_thread": "Continuación de hilo",
|
"status.continued_thread": "Continuación de hilo",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Mensaje eliminado por el autor",
|
"status.quote_error.revoked": "Mensaje eliminado por el autor",
|
||||||
"status.quote_followers_only": "Solo los seguidores pueden citar este mensaje",
|
"status.quote_followers_only": "Solo los seguidores pueden citar este mensaje",
|
||||||
"status.quote_manual_review": "El autor revisará manualmente",
|
"status.quote_manual_review": "El autor revisará manualmente",
|
||||||
|
"status.quote_noun": "Cita",
|
||||||
"status.quote_policy_change": "Cambiá quién puede citar",
|
"status.quote_policy_change": "Cambiá quién puede citar",
|
||||||
"status.quote_post_author": "Se citó un mensaje de @{name}",
|
"status.quote_post_author": "Se citó un mensaje de @{name}",
|
||||||
"status.quote_private": "No se pueden citar los mensajes privados",
|
"status.quote_private": "No se pueden citar los mensajes privados",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Deshacer impulso",
|
"status.cancel_reblog_private": "Deshacer impulso",
|
||||||
"status.cannot_quote": "No está permitido citar esta publicación",
|
"status.cannot_quote": "No está permitido citar esta publicación",
|
||||||
"status.cannot_reblog": "Esta publicación no puede ser impulsada",
|
"status.cannot_reblog": "Esta publicación no puede ser impulsada",
|
||||||
|
"status.contains_quote": "Contiene cita",
|
||||||
"status.context.load_new_replies": "Nuevas respuestas disponibles",
|
"status.context.load_new_replies": "Nuevas respuestas disponibles",
|
||||||
"status.context.loading": "Comprobando si hay más respuestas",
|
"status.context.loading": "Comprobando si hay más respuestas",
|
||||||
"status.continued_thread": "Hilo continuado",
|
"status.continued_thread": "Hilo continuado",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Publicación eliminada por el autor",
|
"status.quote_error.revoked": "Publicación eliminada por el autor",
|
||||||
"status.quote_followers_only": "Solo los seguidores pueden citar esta publicación",
|
"status.quote_followers_only": "Solo los seguidores pueden citar esta publicación",
|
||||||
"status.quote_manual_review": "El autor la revisará manualmente",
|
"status.quote_manual_review": "El autor la revisará manualmente",
|
||||||
|
"status.quote_noun": "Cita",
|
||||||
"status.quote_policy_change": "Cambia quién puede citarte",
|
"status.quote_policy_change": "Cambia quién puede citarte",
|
||||||
"status.quote_post_author": "Ha citado una publicación de @{name}",
|
"status.quote_post_author": "Ha citado una publicación de @{name}",
|
||||||
"status.quote_private": "Las publicaciones privadas no pueden citarse",
|
"status.quote_private": "Las publicaciones privadas no pueden citarse",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Deshacer impulso",
|
"status.cancel_reblog_private": "Deshacer impulso",
|
||||||
"status.cannot_quote": "No tienes permiso para citar esta publicación",
|
"status.cannot_quote": "No tienes permiso para citar esta publicación",
|
||||||
"status.cannot_reblog": "Esta publicación no se puede impulsar",
|
"status.cannot_reblog": "Esta publicación no se puede impulsar",
|
||||||
|
"status.contains_quote": "Contiene cita",
|
||||||
"status.context.load_new_replies": "Hay nuevas respuestas",
|
"status.context.load_new_replies": "Hay nuevas respuestas",
|
||||||
"status.context.loading": "Buscando más respuestas",
|
"status.context.loading": "Buscando más respuestas",
|
||||||
"status.continued_thread": "Continuó el hilo",
|
"status.continued_thread": "Continuó el hilo",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Publicación eliminada por el autor",
|
"status.quote_error.revoked": "Publicación eliminada por el autor",
|
||||||
"status.quote_followers_only": "Solo los seguidores pueden citar esta publicación",
|
"status.quote_followers_only": "Solo los seguidores pueden citar esta publicación",
|
||||||
"status.quote_manual_review": "El autor revisará manualmente",
|
"status.quote_manual_review": "El autor revisará manualmente",
|
||||||
|
"status.quote_noun": "Cita",
|
||||||
"status.quote_policy_change": "Cambia quién puede citarte",
|
"status.quote_policy_change": "Cambia quién puede citarte",
|
||||||
"status.quote_post_author": "Ha citado una publicación de @{name}",
|
"status.quote_post_author": "Ha citado una publicación de @{name}",
|
||||||
"status.quote_private": "Las publicaciones privadas no pueden ser citadas",
|
"status.quote_private": "Las publicaciones privadas no pueden ser citadas",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Lõpeta jagamine",
|
"status.cancel_reblog_private": "Lõpeta jagamine",
|
||||||
"status.cannot_quote": "Sul pole õigust seda postitust tsiteerida",
|
"status.cannot_quote": "Sul pole õigust seda postitust tsiteerida",
|
||||||
"status.cannot_reblog": "Seda postitust ei saa jagada",
|
"status.cannot_reblog": "Seda postitust ei saa jagada",
|
||||||
|
"status.contains_quote": "Sisaldab tsitaati",
|
||||||
"status.context.load_new_replies": "Leidub uusi vastuseid",
|
"status.context.load_new_replies": "Leidub uusi vastuseid",
|
||||||
"status.context.loading": "Kontrollin täiendavate vastuste olemasolu",
|
"status.context.loading": "Kontrollin täiendavate vastuste olemasolu",
|
||||||
"status.continued_thread": "Jätkatud lõim",
|
"status.continued_thread": "Jätkatud lõim",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Autor on postituse eemaldanud",
|
"status.quote_error.revoked": "Autor on postituse eemaldanud",
|
||||||
"status.quote_followers_only": "Vaid jälgijad saavad seda postitust tsiteerida",
|
"status.quote_followers_only": "Vaid jälgijad saavad seda postitust tsiteerida",
|
||||||
"status.quote_manual_review": "Autor vaatab selle üle",
|
"status.quote_manual_review": "Autor vaatab selle üle",
|
||||||
|
"status.quote_noun": "Tsitaat",
|
||||||
"status.quote_policy_change": "Muuda neid, kes võivad tsiteerida",
|
"status.quote_policy_change": "Muuda neid, kes võivad tsiteerida",
|
||||||
"status.quote_post_author": "Tsiteeris kasutaja @{name} postitust",
|
"status.quote_post_author": "Tsiteeris kasutaja @{name} postitust",
|
||||||
"status.quote_private": "Otsepostituste tsiteerimine pole võimalik",
|
"status.quote_private": "Otsepostituste tsiteerimine pole võimalik",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Peru tehostus",
|
"status.cancel_reblog_private": "Peru tehostus",
|
||||||
"status.cannot_quote": "Sinulla ei ole oikeutta lainata tätä julkaisua",
|
"status.cannot_quote": "Sinulla ei ole oikeutta lainata tätä julkaisua",
|
||||||
"status.cannot_reblog": "Tätä julkaisua ei voi tehostaa",
|
"status.cannot_reblog": "Tätä julkaisua ei voi tehostaa",
|
||||||
|
"status.contains_quote": "Sisältää lainauksen",
|
||||||
"status.context.load_new_replies": "Uusia vastauksia saatavilla",
|
"status.context.load_new_replies": "Uusia vastauksia saatavilla",
|
||||||
"status.context.loading": "Tarkistetaan lisävastauksia",
|
"status.context.loading": "Tarkistetaan lisävastauksia",
|
||||||
"status.continued_thread": "Jatkoi ketjua",
|
"status.continued_thread": "Jatkoi ketjua",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Tekijä on poistanut julkaisun",
|
"status.quote_error.revoked": "Tekijä on poistanut julkaisun",
|
||||||
"status.quote_followers_only": "Vain seuraajat voivat lainata tätä julkaisua",
|
"status.quote_followers_only": "Vain seuraajat voivat lainata tätä julkaisua",
|
||||||
"status.quote_manual_review": "Tekijä arvioi pyynnön manuaalisesti",
|
"status.quote_manual_review": "Tekijä arvioi pyynnön manuaalisesti",
|
||||||
|
"status.quote_noun": "Lainaus",
|
||||||
"status.quote_policy_change": "Vaihda, kuka voi lainata",
|
"status.quote_policy_change": "Vaihda, kuka voi lainata",
|
||||||
"status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua",
|
"status.quote_post_author": "Lainaa käyttäjän @{name} julkaisua",
|
||||||
"status.quote_private": "Yksityisiä julkaisuja ei voi lainata",
|
"status.quote_private": "Yksityisiä julkaisuja ei voi lainata",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Strika stimbran",
|
"status.cancel_reblog_private": "Strika stimbran",
|
||||||
"status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin",
|
"status.cannot_quote": "Tú hevur ikki loyvi at sitera hendan postin",
|
||||||
"status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin",
|
"status.cannot_reblog": "Tað ber ikki til at stimbra hendan postin",
|
||||||
|
"status.contains_quote": "Inniheldur sitat",
|
||||||
"status.context.load_new_replies": "Nýggj svar tøk",
|
"status.context.load_new_replies": "Nýggj svar tøk",
|
||||||
"status.context.loading": "Kanni um tað eru fleiri svar",
|
"status.context.loading": "Kanni um tað eru fleiri svar",
|
||||||
"status.continued_thread": "Framhaldandi tráður",
|
"status.continued_thread": "Framhaldandi tráður",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Posturin burturbeindur av høvundinum",
|
"status.quote_error.revoked": "Posturin burturbeindur av høvundinum",
|
||||||
"status.quote_followers_only": "Bara fylgjarar kunnu sitera hendan postin",
|
"status.quote_followers_only": "Bara fylgjarar kunnu sitera hendan postin",
|
||||||
"status.quote_manual_review": "Høvundurin fer at eftirkanna manuelt",
|
"status.quote_manual_review": "Høvundurin fer at eftirkanna manuelt",
|
||||||
|
"status.quote_noun": "Sitat",
|
||||||
"status.quote_policy_change": "Broyt hvør kann sitera",
|
"status.quote_policy_change": "Broyt hvør kann sitera",
|
||||||
"status.quote_post_author": "Siteraði ein post hjá @{name}",
|
"status.quote_post_author": "Siteraði ein post hjá @{name}",
|
||||||
"status.quote_private": "Privatir postar kunnu ikki siterast",
|
"status.quote_private": "Privatir postar kunnu ikki siterast",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Dímhol",
|
"status.cancel_reblog_private": "Dímhol",
|
||||||
"status.cannot_quote": "Ní cheadaítear duit an post seo a lua",
|
"status.cannot_quote": "Ní cheadaítear duit an post seo a lua",
|
||||||
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh",
|
"status.cannot_reblog": "Ní féidir an phostáil seo a mholadh",
|
||||||
|
"status.contains_quote": "Tá luachan ann",
|
||||||
"status.context.load_new_replies": "Freagraí nua ar fáil",
|
"status.context.load_new_replies": "Freagraí nua ar fáil",
|
||||||
"status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí",
|
"status.context.loading": "Ag seiceáil le haghaidh tuilleadh freagraí",
|
||||||
"status.continued_thread": "Snáithe ar lean",
|
"status.continued_thread": "Snáithe ar lean",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Post bainte ag an údar",
|
"status.quote_error.revoked": "Post bainte ag an údar",
|
||||||
"status.quote_followers_only": "Ní féidir ach le leantóirí an post seo a lua",
|
"status.quote_followers_only": "Ní féidir ach le leantóirí an post seo a lua",
|
||||||
"status.quote_manual_review": "Déanfaidh an t-údar athbhreithniú de láimh",
|
"status.quote_manual_review": "Déanfaidh an t-údar athbhreithniú de láimh",
|
||||||
|
"status.quote_noun": "Luachan",
|
||||||
"status.quote_policy_change": "Athraigh cé a fhéadann luachan a thabhairt",
|
"status.quote_policy_change": "Athraigh cé a fhéadann luachan a thabhairt",
|
||||||
"status.quote_post_author": "Luaigh mé post le @{name}",
|
"status.quote_post_author": "Luaigh mé post le @{name}",
|
||||||
"status.quote_private": "Ní féidir poist phríobháideacha a lua",
|
"status.quote_private": "Ní féidir poist phríobháideacha a lua",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Desfacer compartido",
|
"status.cancel_reblog_private": "Desfacer compartido",
|
||||||
"status.cannot_quote": "Non tes permiso para citar esta publicación",
|
"status.cannot_quote": "Non tes permiso para citar esta publicación",
|
||||||
"status.cannot_reblog": "Esta publicación non pode ser promovida",
|
"status.cannot_reblog": "Esta publicación non pode ser promovida",
|
||||||
|
"status.contains_quote": "Contén unha cita",
|
||||||
"status.context.load_new_replies": "Non hai respostas dispoñibles",
|
"status.context.load_new_replies": "Non hai respostas dispoñibles",
|
||||||
"status.context.loading": "Mirando se hai máis respostas",
|
"status.context.loading": "Mirando se hai máis respostas",
|
||||||
"status.continued_thread": "Continua co fío",
|
"status.continued_thread": "Continua co fío",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Publicación retirada pola autora",
|
"status.quote_error.revoked": "Publicación retirada pola autora",
|
||||||
"status.quote_followers_only": "Só as seguidoras poden citar esta publicación",
|
"status.quote_followers_only": "Só as seguidoras poden citar esta publicación",
|
||||||
"status.quote_manual_review": "A autora revisará manualmente",
|
"status.quote_manual_review": "A autora revisará manualmente",
|
||||||
|
"status.quote_noun": "Cita",
|
||||||
"status.quote_policy_change": "Cambia quen pode citarte",
|
"status.quote_policy_change": "Cambia quen pode citarte",
|
||||||
"status.quote_post_author": "Citou unha publicación de @{name}",
|
"status.quote_post_author": "Citou unha publicación de @{name}",
|
||||||
"status.quote_private": "As publicacións privadas non se poden citar",
|
"status.quote_private": "As publicacións privadas non se poden citar",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "הסרת הדהוד",
|
"status.cancel_reblog_private": "הסרת הדהוד",
|
||||||
"status.cannot_quote": "אין לך הרשאה לצטט את ההודעה הזו",
|
"status.cannot_quote": "אין לך הרשאה לצטט את ההודעה הזו",
|
||||||
"status.cannot_reblog": "לא ניתן להדהד חצרוץ זה",
|
"status.cannot_reblog": "לא ניתן להדהד חצרוץ זה",
|
||||||
|
"status.contains_quote": "הודעה מכילה ציטוט",
|
||||||
"status.context.load_new_replies": "הגיעו תגובות חדשות",
|
"status.context.load_new_replies": "הגיעו תגובות חדשות",
|
||||||
"status.context.loading": "מחפש תגובות חדשות",
|
"status.context.loading": "מחפש תגובות חדשות",
|
||||||
"status.continued_thread": "שרשור מתמשך",
|
"status.continued_thread": "שרשור מתמשך",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "ההודעה הוסרה על ידי המחבר.ת",
|
"status.quote_error.revoked": "ההודעה הוסרה על ידי המחבר.ת",
|
||||||
"status.quote_followers_only": "רק עוקביך יוכלו לצטט את ההודעה",
|
"status.quote_followers_only": "רק עוקביך יוכלו לצטט את ההודעה",
|
||||||
"status.quote_manual_review": "מחבר.ת ההודעה יחזרו אליך אחרי בדיקה",
|
"status.quote_manual_review": "מחבר.ת ההודעה יחזרו אליך אחרי בדיקה",
|
||||||
|
"status.quote_noun": "ציטוט",
|
||||||
"status.quote_policy_change": "הגדרת הרשאה לציטוט הודעותיך",
|
"status.quote_policy_change": "הגדרת הרשאה לציטוט הודעותיך",
|
||||||
"status.quote_post_author": "ההודעה צוטטה על ידי @{name}",
|
"status.quote_post_author": "ההודעה צוטטה על ידי @{name}",
|
||||||
"status.quote_private": "הודעות פרטיות לא ניתנות לציטוט",
|
"status.quote_private": "הודעות פרטיות לא ניתנות לציטוט",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Megtolás visszavonása",
|
"status.cancel_reblog_private": "Megtolás visszavonása",
|
||||||
"status.cannot_quote": "Nem idézheted ezt a bejegyzést",
|
"status.cannot_quote": "Nem idézheted ezt a bejegyzést",
|
||||||
"status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni",
|
"status.cannot_reblog": "Ezt a bejegyzést nem lehet megtolni",
|
||||||
|
"status.contains_quote": "Idézést tartalmaz",
|
||||||
"status.context.load_new_replies": "Új válaszok érhetőek el",
|
"status.context.load_new_replies": "Új válaszok érhetőek el",
|
||||||
"status.context.loading": "További válaszok keresése",
|
"status.context.loading": "További válaszok keresése",
|
||||||
"status.continued_thread": "Folytatott szál",
|
"status.continued_thread": "Folytatott szál",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "A szerző eltávolítta a bejegyzést",
|
"status.quote_error.revoked": "A szerző eltávolítta a bejegyzést",
|
||||||
"status.quote_followers_only": "Csak a követők idézhetik ezt a bejegyzést",
|
"status.quote_followers_only": "Csak a követők idézhetik ezt a bejegyzést",
|
||||||
"status.quote_manual_review": "A szerző kézileg fogja jóváhagyni",
|
"status.quote_manual_review": "A szerző kézileg fogja jóváhagyni",
|
||||||
|
"status.quote_noun": "Idézés",
|
||||||
"status.quote_policy_change": "Módosítás, hogy kik idézhetnek",
|
"status.quote_policy_change": "Módosítás, hogy kik idézhetnek",
|
||||||
"status.quote_post_author": "Idézte @{name} bejegyzését",
|
"status.quote_post_author": "Idézte @{name} bejegyzését",
|
||||||
"status.quote_private": "A privát bejegyzések nem idézhetőek",
|
"status.quote_private": "A privát bejegyzések nem idézhetőek",
|
||||||
|
|||||||
@ -642,21 +642,21 @@
|
|||||||
"notifications.column_settings.alert": "デスクトップ通知",
|
"notifications.column_settings.alert": "デスクトップ通知",
|
||||||
"notifications.column_settings.favourite": "お気に入り",
|
"notifications.column_settings.favourite": "お気に入り",
|
||||||
"notifications.column_settings.filter_bar.advanced": "すべてのカテゴリを表示",
|
"notifications.column_settings.filter_bar.advanced": "すべてのカテゴリを表示",
|
||||||
"notifications.column_settings.filter_bar.category": "クイックフィルターバー:",
|
"notifications.column_settings.filter_bar.category": "クイックフィルターバー",
|
||||||
"notifications.column_settings.follow": "新しいフォロワー",
|
"notifications.column_settings.follow": "新しいフォロワー",
|
||||||
"notifications.column_settings.follow_request": "新しいフォローリクエスト:",
|
"notifications.column_settings.follow_request": "新しいフォローリクエスト",
|
||||||
"notifications.column_settings.group": "グループ",
|
"notifications.column_settings.group": "グループ",
|
||||||
"notifications.column_settings.mention": "返信",
|
"notifications.column_settings.mention": "返信",
|
||||||
"notifications.column_settings.poll": "アンケート結果",
|
"notifications.column_settings.poll": "アンケート結果",
|
||||||
"notifications.column_settings.push": "プッシュ通知",
|
"notifications.column_settings.push": "プッシュ通知",
|
||||||
"notifications.column_settings.quote": "引用",
|
"notifications.column_settings.quote": "引用",
|
||||||
"notifications.column_settings.reblog": "ブースト:",
|
"notifications.column_settings.reblog": "ブースト",
|
||||||
"notifications.column_settings.show": "カラムに表示",
|
"notifications.column_settings.show": "カラムに表示",
|
||||||
"notifications.column_settings.sound": "通知音を再生",
|
"notifications.column_settings.sound": "通知音を再生",
|
||||||
"notifications.column_settings.status": "新しい投稿",
|
"notifications.column_settings.status": "新しい投稿",
|
||||||
"notifications.column_settings.unread_notifications.category": "未読の通知:",
|
"notifications.column_settings.unread_notifications.category": "未読の通知",
|
||||||
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
|
"notifications.column_settings.unread_notifications.highlight": "未読の通知を強調表示",
|
||||||
"notifications.column_settings.update": "編集:",
|
"notifications.column_settings.update": "編集",
|
||||||
"notifications.filter.all": "すべて",
|
"notifications.filter.all": "すべて",
|
||||||
"notifications.filter.boosts": "ブースト",
|
"notifications.filter.boosts": "ブースト",
|
||||||
"notifications.filter.favourites": "お気に入り",
|
"notifications.filter.favourites": "お気に入り",
|
||||||
@ -875,6 +875,7 @@
|
|||||||
"status.quote.cancel": "引用をキャンセル",
|
"status.quote.cancel": "引用をキャンセル",
|
||||||
"status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています",
|
"status.quote_error.filtered": "あなたのフィルター設定によって非表示になっています",
|
||||||
"status.quote_error.pending_approval": "承認待ちの投稿",
|
"status.quote_error.pending_approval": "承認待ちの投稿",
|
||||||
|
"status.quote_noun": "引用",
|
||||||
"status.quotes": "{count, plural, other {引用}}",
|
"status.quotes": "{count, plural, other {引用}}",
|
||||||
"status.read_more": "もっと見る",
|
"status.read_more": "もっと見る",
|
||||||
"status.reblog": "ブースト",
|
"status.reblog": "ブースト",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Niet langer boosten",
|
"status.cancel_reblog_private": "Niet langer boosten",
|
||||||
"status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren",
|
"status.cannot_quote": "Je bent niet gemachtigd om dit bericht te citeren",
|
||||||
"status.cannot_reblog": "Dit bericht kan niet geboost worden",
|
"status.cannot_reblog": "Dit bericht kan niet geboost worden",
|
||||||
|
"status.contains_quote": "Bevat citaat",
|
||||||
"status.context.load_new_replies": "Nieuwe reacties beschikbaar",
|
"status.context.load_new_replies": "Nieuwe reacties beschikbaar",
|
||||||
"status.context.loading": "Op nieuwe reacties aan het controleren",
|
"status.context.loading": "Op nieuwe reacties aan het controleren",
|
||||||
"status.continued_thread": "Vervolg van gesprek",
|
"status.continued_thread": "Vervolg van gesprek",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Bericht verwijderd door auteur",
|
"status.quote_error.revoked": "Bericht verwijderd door auteur",
|
||||||
"status.quote_followers_only": "Alleen volgers mogen dit bericht citeren",
|
"status.quote_followers_only": "Alleen volgers mogen dit bericht citeren",
|
||||||
"status.quote_manual_review": "De auteur gaat het handmatig beoordelen",
|
"status.quote_manual_review": "De auteur gaat het handmatig beoordelen",
|
||||||
|
"status.quote_noun": "Citaat",
|
||||||
"status.quote_policy_change": "Wijzig wie jou mag citeren",
|
"status.quote_policy_change": "Wijzig wie jou mag citeren",
|
||||||
"status.quote_post_author": "Citeerde een bericht van @{name}",
|
"status.quote_post_author": "Citeerde een bericht van @{name}",
|
||||||
"status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk",
|
"status.quote_private": "Citeren van berichten aan alleen volgers is niet mogelijk",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
|
"status.cancel_reblog_private": "Yeniden paylaşımı geri al",
|
||||||
"status.cannot_quote": "Bu gönderiyi alıntılamaya izniniz yok",
|
"status.cannot_quote": "Bu gönderiyi alıntılamaya izniniz yok",
|
||||||
"status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
|
"status.cannot_reblog": "Bu gönderi yeniden paylaşılamaz",
|
||||||
|
"status.contains_quote": "Alıntı içeriyor",
|
||||||
"status.context.load_new_replies": "Yeni yanıtlar mevcut",
|
"status.context.load_new_replies": "Yeni yanıtlar mevcut",
|
||||||
"status.context.loading": "Daha fazla yanıt için kontrol ediliyor",
|
"status.context.loading": "Daha fazla yanıt için kontrol ediliyor",
|
||||||
"status.continued_thread": "Devam eden akış",
|
"status.continued_thread": "Devam eden akış",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Gönderi yazarı tarafından kaldırıldı",
|
"status.quote_error.revoked": "Gönderi yazarı tarafından kaldırıldı",
|
||||||
"status.quote_followers_only": "Sadece takipçiler bu gönderiyi alıntılayabilir",
|
"status.quote_followers_only": "Sadece takipçiler bu gönderiyi alıntılayabilir",
|
||||||
"status.quote_manual_review": "Yazar manuel olarak gözden geçirecek",
|
"status.quote_manual_review": "Yazar manuel olarak gözden geçirecek",
|
||||||
|
"status.quote_noun": "Alıntı",
|
||||||
"status.quote_policy_change": "Kimin alıntı yapabileceğini değiştirin",
|
"status.quote_policy_change": "Kimin alıntı yapabileceğini değiştirin",
|
||||||
"status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı",
|
"status.quote_post_author": "@{name} adlı kullanıcının bir gönderisini alıntıladı",
|
||||||
"status.quote_private": "Özel gönderiler alıntılanamaz",
|
"status.quote_private": "Özel gönderiler alıntılanamaz",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "Bỏ đăng lại",
|
"status.cancel_reblog_private": "Bỏ đăng lại",
|
||||||
"status.cannot_quote": "Bạn không được phép trích dẫn tút này",
|
"status.cannot_quote": "Bạn không được phép trích dẫn tút này",
|
||||||
"status.cannot_reblog": "Không thể đăng lại tút này",
|
"status.cannot_reblog": "Không thể đăng lại tút này",
|
||||||
|
"status.contains_quote": "Chứa trích dẫn",
|
||||||
"status.context.load_new_replies": "Có những trả lời mới",
|
"status.context.load_new_replies": "Có những trả lời mới",
|
||||||
"status.context.loading": "Kiểm tra nhiều trả lời hơn",
|
"status.context.loading": "Kiểm tra nhiều trả lời hơn",
|
||||||
"status.continued_thread": "Tiếp tục chủ đề",
|
"status.continued_thread": "Tiếp tục chủ đề",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "Tút gốc đã bị tác giả gỡ",
|
"status.quote_error.revoked": "Tút gốc đã bị tác giả gỡ",
|
||||||
"status.quote_followers_only": "Chỉ người theo dõi tôi có thể trích dẫn tút này",
|
"status.quote_followers_only": "Chỉ người theo dõi tôi có thể trích dẫn tút này",
|
||||||
"status.quote_manual_review": "Người đăng sẽ duyệt thủ công",
|
"status.quote_manual_review": "Người đăng sẽ duyệt thủ công",
|
||||||
|
"status.quote_noun": "Trích dẫn",
|
||||||
"status.quote_policy_change": "Thay đổi người có thể trích dẫn",
|
"status.quote_policy_change": "Thay đổi người có thể trích dẫn",
|
||||||
"status.quote_post_author": "Trích dẫn từ tút của @{name}",
|
"status.quote_post_author": "Trích dẫn từ tút của @{name}",
|
||||||
"status.quote_private": "Không thể trích dẫn nhắn riêng",
|
"status.quote_private": "Không thể trích dẫn nhắn riêng",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "取消转嘟",
|
"status.cancel_reblog_private": "取消转嘟",
|
||||||
"status.cannot_quote": "你无法引用此嘟文",
|
"status.cannot_quote": "你无法引用此嘟文",
|
||||||
"status.cannot_reblog": "不能转嘟这条嘟文",
|
"status.cannot_reblog": "不能转嘟这条嘟文",
|
||||||
|
"status.contains_quote": "包含引用",
|
||||||
"status.context.load_new_replies": "有新回复",
|
"status.context.load_new_replies": "有新回复",
|
||||||
"status.context.loading": "正在检查更多回复",
|
"status.context.loading": "正在检查更多回复",
|
||||||
"status.continued_thread": "上接嘟文串",
|
"status.continued_thread": "上接嘟文串",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "嘟文已被作者删除",
|
"status.quote_error.revoked": "嘟文已被作者删除",
|
||||||
"status.quote_followers_only": "只有关注者才能引用这篇嘟文",
|
"status.quote_followers_only": "只有关注者才能引用这篇嘟文",
|
||||||
"status.quote_manual_review": "嘟文作者将人工审核",
|
"status.quote_manual_review": "嘟文作者将人工审核",
|
||||||
|
"status.quote_noun": "引用",
|
||||||
"status.quote_policy_change": "更改谁可以引用",
|
"status.quote_policy_change": "更改谁可以引用",
|
||||||
"status.quote_post_author": "引用了 @{name} 的嘟文",
|
"status.quote_post_author": "引用了 @{name} 的嘟文",
|
||||||
"status.quote_private": "不能引用私人嘟文",
|
"status.quote_private": "不能引用私人嘟文",
|
||||||
|
|||||||
@ -864,6 +864,7 @@
|
|||||||
"status.cancel_reblog_private": "取消轉嘟",
|
"status.cancel_reblog_private": "取消轉嘟",
|
||||||
"status.cannot_quote": "您不被允許引用此嘟文",
|
"status.cannot_quote": "您不被允許引用此嘟文",
|
||||||
"status.cannot_reblog": "這則嘟文無法被轉嘟",
|
"status.cannot_reblog": "這則嘟文無法被轉嘟",
|
||||||
|
"status.contains_quote": "包含引用嘟文",
|
||||||
"status.context.load_new_replies": "有新回嘟",
|
"status.context.load_new_replies": "有新回嘟",
|
||||||
"status.context.loading": "正在檢查更多回嘟",
|
"status.context.loading": "正在檢查更多回嘟",
|
||||||
"status.continued_thread": "接續討論串",
|
"status.continued_thread": "接續討論串",
|
||||||
@ -903,6 +904,7 @@
|
|||||||
"status.quote_error.revoked": "嘟文已被作者刪除",
|
"status.quote_error.revoked": "嘟文已被作者刪除",
|
||||||
"status.quote_followers_only": "只有我的跟隨者能引用此嘟文",
|
"status.quote_followers_only": "只有我的跟隨者能引用此嘟文",
|
||||||
"status.quote_manual_review": "嘟文作者將人工審閱",
|
"status.quote_manual_review": "嘟文作者將人工審閱",
|
||||||
|
"status.quote_noun": "引用嘟文",
|
||||||
"status.quote_policy_change": "變更可以引用的人",
|
"status.quote_policy_change": "變更可以引用的人",
|
||||||
"status.quote_post_author": "已引用 @{name} 之嘟文",
|
"status.quote_post_author": "已引用 @{name} 之嘟文",
|
||||||
"status.quote_private": "無法引用私人嘟文",
|
"status.quote_private": "無法引用私人嘟文",
|
||||||
|
|||||||
@ -12,11 +12,7 @@ export function isProduction() {
|
|||||||
else return import.meta.env.PROD;
|
else return import.meta.env.PROD;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Features =
|
export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures';
|
||||||
| 'modern_emojis'
|
|
||||||
| 'outgoing_quotes'
|
|
||||||
| 'fasp'
|
|
||||||
| 'http_message_signatures';
|
|
||||||
|
|
||||||
export function isFeatureEnabled(feature: Features) {
|
export function isFeatureEnabled(feature: Features) {
|
||||||
return initialState?.features.includes(feature) ?? false;
|
return initialState?.features.includes(feature) ?? false;
|
||||||
|
|||||||
@ -163,7 +163,7 @@ $content-width: 840px;
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: $content-width + $sidebar-width) {
|
@media screen and (max-width: ($content-width + $sidebar-width)) {
|
||||||
.sidebar-wrapper--empty {
|
.sidebar-wrapper--empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@ -1081,6 +1081,17 @@ a.name-tag,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__action-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
&:not(.no-wrap) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
padding: 0 15px;
|
padding: 0 15px;
|
||||||
color: $dark-text-color;
|
color: $dark-text-color;
|
||||||
@ -1097,10 +1108,8 @@ a.name-tag,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-bar {
|
&__actions {
|
||||||
display: flex;
|
margin-inline-start: auto;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__permissions {
|
&__permissions {
|
||||||
|
|||||||
@ -1597,6 +1597,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-reduce-motion &--highlighted-entry::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgb(from $ui-highlight-color r g b / 20%);
|
||||||
|
opacity: 0;
|
||||||
|
animation: fade 0.7s reverse both 0.3s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status__relative-time {
|
.status__relative-time {
|
||||||
@ -2960,7 +2969,6 @@ a.account__display-name {
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.unscrollable {
|
&.unscrollable {
|
||||||
@ -3136,6 +3144,29 @@ a.account__display-name {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.column__alert {
|
||||||
|
position: sticky;
|
||||||
|
bottom: 1rem;
|
||||||
|
z-index: 10;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px;
|
||||||
|
padding-inline: 10px;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-inline: auto;
|
||||||
|
|
||||||
|
@media (max-width: #{$mobile-menu-breakpoint - 1}) {
|
||||||
|
bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
// Make all nested alerts occupy the same space
|
||||||
|
// rather than stack
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.ui {
|
.ui {
|
||||||
--mobile-bottom-nav-height: 55px;
|
--mobile-bottom-nav-height: 55px;
|
||||||
--last-content-item-border-width: 2px;
|
--last-content-item-border-width: 2px;
|
||||||
@ -3176,7 +3207,6 @@ a.account__display-name {
|
|||||||
.column,
|
.column,
|
||||||
.drawer {
|
.drawer {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (width > $mobile-breakpoint) {
|
@media screen and (width > $mobile-breakpoint) {
|
||||||
@ -10388,6 +10418,21 @@ noscript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-bar__loading-indicator {
|
||||||
|
--spinner-size: 22px;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
height: var(--spinner-size);
|
||||||
|
width: var(--spinner-size);
|
||||||
|
margin-inline-start: 2px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
color: $white;
|
||||||
|
height: var(--spinner-size);
|
||||||
|
width: var(--spinner-size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hashtag-header {
|
.hashtag-header {
|
||||||
border-bottom: 1px solid var(--background-border-color);
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
|
|||||||
@ -9,7 +9,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
|
|||||||
quoted_status = status_from_uri(object_uri)
|
quoted_status = status_from_uri(object_uri)
|
||||||
return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable?
|
return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable?
|
||||||
|
|
||||||
if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote?
|
if StatusPolicy.new(@account, quoted_status).quote?
|
||||||
accept_quote_request!(quoted_status)
|
accept_quote_request!(quoted_status)
|
||||||
else
|
else
|
||||||
reject_quote_request!(quoted_status)
|
reject_quote_request!(quoted_status)
|
||||||
|
|||||||
@ -38,7 +38,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
|
|||||||
attribute :quote, key: :quote_uri, if: :quote?
|
attribute :quote, key: :quote_uri, if: :quote?
|
||||||
attribute :quote_authorization, if: :quote_authorization?
|
attribute :quote_authorization, if: :quote_authorization?
|
||||||
|
|
||||||
attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
|
attribute :interaction_policy
|
||||||
|
|
||||||
def id
|
def id
|
||||||
raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only]
|
raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only]
|
||||||
|
|||||||
@ -6,5 +6,5 @@ class REST::ShallowStatusSerializer < REST::StatusSerializer
|
|||||||
# It looks like redefining one `has_one` requires redefining all inherited ones
|
# It looks like redefining one `has_one` requires redefining all inherited ones
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
|
has_one :quote_approval
|
||||||
end
|
end
|
||||||
|
|||||||
@ -36,7 +36,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
|
|||||||
has_one :quote, key: :quote, serializer: REST::QuoteSerializer
|
has_one :quote, key: :quote, serializer: REST::QuoteSerializer
|
||||||
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
|
||||||
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
|
||||||
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
|
has_one :quote_approval
|
||||||
|
|
||||||
delegate :local?, to: :object
|
delegate :local?, to: :object
|
||||||
|
|
||||||
|
|||||||
@ -85,7 +85,7 @@ class PostStatusService < BaseService
|
|||||||
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
|
@sensitive = (@options[:sensitive].nil? ? @account.user&.setting_default_sensitive : @options[:sensitive]) || @options[:spoiler_text].present?
|
||||||
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
@visibility = @options[:visibility] || @account.user&.setting_default_privacy
|
||||||
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
|
@visibility = :unlisted if @visibility&.to_sym == :public && @account.silenced?
|
||||||
@visibility = :private if @quoted_status&.private_visibility?
|
@visibility = :private if @quoted_status&.private_visibility? && %i(public unlisted).include?(@visibility&.to_sym)
|
||||||
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
@scheduled_at = @options[:scheduled_at]&.to_datetime
|
||||||
@scheduled_at = nil if scheduled_in_the_past?
|
@scheduled_at = nil if scheduled_in_the_past?
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
|
|||||||
@ -9,7 +9,7 @@
|
|||||||
- else
|
- else
|
||||||
= l(announcement.created_at)
|
= l(announcement.created_at)
|
||||||
|
|
||||||
%div
|
.announcements-list__item__actions
|
||||||
- if can?(:distribute, announcement)
|
- if can?(:distribute, announcement)
|
||||||
= table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement)
|
= table_link_to 'mail', t('admin.terms_of_service.notify_users'), admin_announcement_preview_path(announcement)
|
||||||
- if can?(:update, announcement)
|
- if can?(:update, announcement)
|
||||||
|
|||||||
@ -26,5 +26,5 @@
|
|||||||
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
|
= link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_ids: role.id)
|
||||||
·
|
·
|
||||||
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
%abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
|
||||||
%div
|
.announcements-list__item__actions
|
||||||
= table_link_to 'edit', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
|
= table_link_to 'edit', t('admin.accounts.edit'), edit_admin_role_path(role) if can?(:update, role)
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
#{rule_counter + 1}.
|
#{rule_counter + 1}.
|
||||||
= truncate(rule.text)
|
= truncate(rule.text)
|
||||||
|
|
||||||
.announcements-list__item__action-bar
|
.announcements-list__item__action-bar.no-wrap
|
||||||
.announcements-list__item__meta
|
.announcements-list__item__meta
|
||||||
= rule.hint
|
= rule.hint
|
||||||
|
|
||||||
|
|||||||
@ -6,5 +6,5 @@
|
|||||||
.announcements-list__item__meta
|
.announcements-list__item__meta
|
||||||
= truncate(warning_preset.text)
|
= truncate(warning_preset.text)
|
||||||
|
|
||||||
%div
|
.announcements-list__item__actions
|
||||||
= table_link_to 'delete', t('admin.warning_presets.delete'), admin_warning_preset_path(warning_preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, warning_preset)
|
= table_link_to 'delete', t('admin.warning_presets.delete'), admin_warning_preset_path(warning_preset), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, warning_preset)
|
||||||
|
|||||||
@ -14,6 +14,6 @@
|
|||||||
|
|
||||||
%abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size)
|
%abbr{ title: webhook.events.join(', ') }= t('admin.webhooks.enabled_events', count: webhook.events.size)
|
||||||
|
|
||||||
%div
|
.announcements-list__item__actions
|
||||||
= table_link_to 'edit', t('admin.webhooks.edit'), edit_admin_webhook_path(webhook) if can?(:update, webhook)
|
= table_link_to 'edit', t('admin.webhooks.edit'), edit_admin_webhook_path(webhook) if can?(:update, webhook)
|
||||||
= table_link_to 'delete', t('admin.webhooks.delete'), admin_webhook_path(webhook), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, webhook)
|
= table_link_to 'delete', t('admin.webhooks.delete'), admin_webhook_path(webhook), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:destroy, webhook)
|
||||||
|
|||||||
@ -32,10 +32,10 @@
|
|||||||
.permissions-list__item__text__type
|
.permissions-list__item__text__type
|
||||||
= t('filters.index.statuses_long', count: filter.statuses.size)
|
= t('filters.index.statuses_long', count: filter.statuses.size)
|
||||||
|
|
||||||
.announcements-list__item__action-bar
|
.filters-list__item__action-bar
|
||||||
.announcements-list__item__meta
|
.filters-list__item__meta
|
||||||
= t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', '))
|
= t('filters.index.contexts', contexts: filter.context.map { |context| I18n.t("filters.contexts.#{context}") }.join(', '))
|
||||||
|
|
||||||
%div
|
.filters-list__item__actions
|
||||||
= table_link_to 'edit', t('filters.edit.title'), edit_filter_path(filter)
|
= table_link_to 'edit', t('filters.edit.title'), edit_filter_path(filter)
|
||||||
= table_link_to 'close', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
= table_link_to 'close', t('filters.index.delete'), filter_path(filter), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
|
= t('doorkeeper.authorized_applications.index.authorized_at', date: l(application.created_at.to_date))
|
||||||
|
|
||||||
- unless application.superapp? || current_account.unavailable?
|
- unless application.superapp? || current_account.unavailable?
|
||||||
%div
|
.announcements-list__item__actions
|
||||||
= table_link_to 'close', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }
|
= table_link_to 'close', t('doorkeeper.authorized_applications.buttons.revoke'), oauth_authorized_application_path(application), method: :delete, data: { confirm: t('doorkeeper.authorized_applications.confirmations.revoke') }
|
||||||
|
|
||||||
.announcements-list__item__permissions
|
.announcements-list__item__permissions
|
||||||
|
|||||||
@ -114,7 +114,7 @@ el:
|
|||||||
other: Αυτός ο λογαριασμός έχει <strong>%{count}</strong> παραπτώματα.
|
other: Αυτός ο λογαριασμός έχει <strong>%{count}</strong> παραπτώματα.
|
||||||
promote: Προαγωγή
|
promote: Προαγωγή
|
||||||
protocol: Πρωτόκολλο
|
protocol: Πρωτόκολλο
|
||||||
public: Δημόσιο
|
public: Δημόσιος
|
||||||
push_subscription_expires: Η εγγραφή PuSH λήγει
|
push_subscription_expires: Η εγγραφή PuSH λήγει
|
||||||
redownload: Ανανέωση άβαταρ
|
redownload: Ανανέωση άβαταρ
|
||||||
redownloaded_msg: Επιτυχής ανανέωση προφίλ του/της %{username} από την πηγή
|
redownloaded_msg: Επιτυχής ανανέωση προφίλ του/της %{username} από την πηγή
|
||||||
@ -1919,7 +1919,7 @@ el:
|
|||||||
visibilities:
|
visibilities:
|
||||||
direct: Ιδιωτική επισήμανση
|
direct: Ιδιωτική επισήμανση
|
||||||
private: Μόνο ακόλουθοι
|
private: Μόνο ακόλουθοι
|
||||||
public: Δημόσιο
|
public: Δημόσια
|
||||||
public_long: Όλοι εντός και εκτός του Mastodon
|
public_long: Όλοι εντός και εκτός του Mastodon
|
||||||
unlisted: Ήσυχα δημόσια
|
unlisted: Ήσυχα δημόσια
|
||||||
unlisted_long: Κρυμμένη από τα αποτελέσματα αναζήτησης Mastodon, τις τάσεις και τις δημόσιες ροές
|
unlisted_long: Κρυμμένη από τα αποτελέσματα αναζήτησης Mastodon, τις τάσεις και τις δημόσιες ροές
|
||||||
|
|||||||
@ -572,6 +572,7 @@ ja:
|
|||||||
title: モデレーション
|
title: モデレーション
|
||||||
moderation_notes:
|
moderation_notes:
|
||||||
create: モデレーションノートを追加
|
create: モデレーションノートを追加
|
||||||
|
title: モデレーションメモ
|
||||||
private_comment: コメント (非公開)
|
private_comment: コメント (非公開)
|
||||||
public_comment: コメント (公開)
|
public_comment: コメント (公開)
|
||||||
purge: パージ
|
purge: パージ
|
||||||
@ -1067,14 +1068,18 @@ ja:
|
|||||||
trending: トレンド
|
trending: トレンド
|
||||||
username_blocks:
|
username_blocks:
|
||||||
add_new: ルールを作成
|
add_new: ルールを作成
|
||||||
|
block_registrations: 登録拒否
|
||||||
comparison:
|
comparison:
|
||||||
contains: 含む
|
contains: 含む
|
||||||
equals: 一致
|
equals: 一致
|
||||||
|
contains_html: "%{string}を含む"
|
||||||
delete: 削除
|
delete: 削除
|
||||||
edit:
|
edit:
|
||||||
title: ユーザー名ルールの編集
|
title: ユーザー名ルールの編集
|
||||||
|
matches_exactly_html: "%{string}に一致"
|
||||||
new:
|
new:
|
||||||
create: ルールを作成
|
create: ルールを作成
|
||||||
|
title: ユーザー名ルール
|
||||||
warning_presets:
|
warning_presets:
|
||||||
add_new: 追加
|
add_new: 追加
|
||||||
delete: 削除
|
delete: 削除
|
||||||
@ -1679,6 +1684,7 @@ ja:
|
|||||||
self_vote: 自分のアンケートには解答できません
|
self_vote: 自分のアンケートには解答できません
|
||||||
too_few_options: は複数必要です
|
too_few_options: は複数必要です
|
||||||
too_many_options: は%{max}個までです
|
too_many_options: は%{max}個までです
|
||||||
|
vote: 投票
|
||||||
preferences:
|
preferences:
|
||||||
other: その他
|
other: その他
|
||||||
posting_defaults: デフォルトの投稿設定
|
posting_defaults: デフォルトの投稿設定
|
||||||
|
|||||||
@ -27,7 +27,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
on_worker_boot do
|
before_worker_boot do
|
||||||
# Ruby process metrics (memory, GC, etc)
|
# Ruby process metrics (memory, GC, etc)
|
||||||
PrometheusExporter::Instrumentation::Process.start(type: 'puma')
|
PrometheusExporter::Instrumentation::Process.start(type: 'puma')
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ if ENV['MASTODON_PROMETHEUS_EXPORTER_ENABLED'] == 'true'
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
on_worker_boot do
|
before_worker_boot do
|
||||||
ActiveSupport.on_load(:active_record) do
|
ActiveSupport.on_load(:active_record) do
|
||||||
ActiveRecord::Base.establish_connection
|
ActiveRecord::Base.establish_connection
|
||||||
end
|
end
|
||||||
|
|||||||
@ -45,7 +45,7 @@ module Mastodon
|
|||||||
|
|
||||||
def api_versions
|
def api_versions
|
||||||
{
|
{
|
||||||
mastodon: Mastodon::Feature.outgoing_quotes_enabled? ? 7 : 6,
|
mastodon: 7,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do
|
RSpec.describe ActivityPub::Activity::QuoteRequest do
|
||||||
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
let(:sender) { Fabricate(:account, domain: 'example.com') }
|
||||||
let(:recipient) { Fabricate(:account) }
|
let(:recipient) { Fabricate(:account) }
|
||||||
let(:quoted_post) { Fabricate(:status, account: recipient) }
|
let(:quoted_post) { Fabricate(:status, account: recipient) }
|
||||||
|
|||||||
@ -28,7 +28,7 @@ RSpec.describe StatusCacheHydrator do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when handling a status with a quote policy', feature: :outgoing_quotes do
|
context 'when handling a status with a quote policy' do
|
||||||
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe 'Interaction policies', feature: :outgoing_quotes do
|
RSpec.describe 'Interaction policies' do
|
||||||
let(:user) { Fabricate(:user) }
|
let(:user) { Fabricate(:user) }
|
||||||
let(:scopes) { 'write:statuses' }
|
let(:scopes) { 'write:statuses' }
|
||||||
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }
|
||||||
|
|||||||
@ -158,7 +158,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without a quote policy', feature: :outgoing_quotes do
|
context 'without a quote policy' do
|
||||||
let(:user) do
|
let(:user) do
|
||||||
Fabricate(:user, settings: { default_quote_policy: 'followers' })
|
Fabricate(:user, settings: { default_quote_policy: 'followers' })
|
||||||
end
|
end
|
||||||
@ -180,7 +180,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'without a quote policy and the user defaults to nobody', feature: :outgoing_quotes do
|
context 'without a quote policy and the user defaults to nobody' do
|
||||||
let(:user) do
|
let(:user) do
|
||||||
Fabricate(:user, settings: { default_quote_policy: 'nobody' })
|
Fabricate(:user, settings: { default_quote_policy: 'nobody' })
|
||||||
end
|
end
|
||||||
@ -202,7 +202,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a quote policy', feature: :outgoing_quotes do
|
context 'with a quote policy' do
|
||||||
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
@ -227,7 +227,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a self-quote post', feature: :outgoing_quotes do
|
context 'with a self-quote post' do
|
||||||
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
@ -248,7 +248,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a self-quote post and a CW but no text', feature: :outgoing_quotes do
|
context 'with a self-quote post and a CW but no text' do
|
||||||
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
let(:quoted_status) { Fabricate(:status, account: user.account) }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
@ -420,7 +420,7 @@ RSpec.describe '/api/v1/statuses' do
|
|||||||
context 'when updating only the quote policy' do
|
context 'when updating only the quote policy' do
|
||||||
let(:params) { { status: status.text, quote_approval_policy: 'public' } }
|
let(:params) { { status: status.text, quote_approval_policy: 'public' } }
|
||||||
|
|
||||||
it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do
|
it 'updates the status', :aggregate_failures do
|
||||||
expect { subject }
|
expect { subject }
|
||||||
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@ RSpec.describe ActivityPub::NoteSerializer do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a quote policy', feature: :outgoing_quotes do
|
context 'with a quote policy' do
|
||||||
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
|
||||||
|
|
||||||
it 'has the expected shape' do
|
it 'has the expected shape' do
|
||||||
|
|||||||
@ -321,6 +321,14 @@ RSpec.describe PostStatusService do
|
|||||||
expect(status).to be_private_visibility
|
expect(status).to be_private_visibility
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'correctly preserves visibility for private mentions self-quoting private posts' do
|
||||||
|
account = Fabricate(:account)
|
||||||
|
quoted_status = Fabricate(:status, account: account, visibility: :private)
|
||||||
|
|
||||||
|
status = subject.call(account, text: 'test', quoted_status: quoted_status, visibility: 'direct')
|
||||||
|
expect(status).to be_direct_visibility
|
||||||
|
end
|
||||||
|
|
||||||
it 'returns existing status when used twice with idempotency key' do
|
it 'returns existing status when used twice with idempotency key' do
|
||||||
account = Fabricate(:account)
|
account = Fabricate(:account)
|
||||||
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
status1 = subject.call(account, text: 'test', idempotency: 'meepmeep')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user