mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-09 05:06:14 +00:00
Merge branch 'main' into patch-1
This commit is contained in:
commit
838b5ffcfb
|
@ -395,7 +395,7 @@ GEM
|
||||||
rexml
|
rexml
|
||||||
link_header (0.0.8)
|
link_header (0.0.8)
|
||||||
lint_roller (1.1.0)
|
lint_roller (1.1.0)
|
||||||
linzer (0.6.4)
|
linzer (0.6.5)
|
||||||
openssl (~> 3.0, >= 3.0.0)
|
openssl (~> 3.0, >= 3.0.0)
|
||||||
rack (>= 2.2, < 4.0)
|
rack (>= 2.2, < 4.0)
|
||||||
starry (~> 0.2)
|
starry (~> 0.2)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import {
|
||||||
muteAccount,
|
muteAccount,
|
||||||
unmuteAccount,
|
unmuteAccount,
|
||||||
} from 'mastodon/actions/accounts';
|
} from 'mastodon/actions/accounts';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { initMuteModal } from 'mastodon/actions/mutes';
|
import { initMuteModal } from 'mastodon/actions/mutes';
|
||||||
import { Avatar } from 'mastodon/components/avatar';
|
import { Avatar } from 'mastodon/components/avatar';
|
||||||
import { Button } from 'mastodon/components/button';
|
import { Button } from 'mastodon/components/button';
|
||||||
|
@ -23,7 +24,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||||
import { ShortNumber } from 'mastodon/components/short_number';
|
import { ShortNumber } from 'mastodon/components/short_number';
|
||||||
import { Skeleton } from 'mastodon/components/skeleton';
|
import { Skeleton } from 'mastodon/components/skeleton';
|
||||||
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
import { VerifiedBadge } from 'mastodon/components/verified_badge';
|
||||||
import { me } from 'mastodon/initial_state';
|
import type { MenuItem } from 'mastodon/models/dropdown_menu';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -46,6 +47,14 @@ const messages = defineMessages({
|
||||||
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
|
||||||
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
block: { id: 'account.block_short', defaultMessage: 'Block' },
|
||||||
more: { id: 'status.more', defaultMessage: 'More' },
|
more: { id: 'status.more', defaultMessage: 'More' },
|
||||||
|
addToLists: {
|
||||||
|
id: 'account.add_or_remove_from_list',
|
||||||
|
defaultMessage: 'Add or Remove from lists',
|
||||||
|
},
|
||||||
|
openOriginalPage: {
|
||||||
|
id: 'account.open_original_page',
|
||||||
|
defaultMessage: 'Open original page',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Account: React.FC<{
|
export const Account: React.FC<{
|
||||||
|
@ -60,6 +69,7 @@ export const Account: React.FC<{
|
||||||
const account = useAppSelector((state) => state.accounts.get(id));
|
const account = useAppSelector((state) => state.accounts.get(id));
|
||||||
const relationship = useAppSelector((state) => state.relationships.get(id));
|
const relationship = useAppSelector((state) => state.relationships.get(id));
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const accountUrl = account?.url;
|
||||||
|
|
||||||
const handleBlock = useCallback(() => {
|
const handleBlock = useCallback(() => {
|
||||||
if (relationship?.blocking) {
|
if (relationship?.blocking) {
|
||||||
|
@ -77,13 +87,62 @@ export const Account: React.FC<{
|
||||||
}
|
}
|
||||||
}, [dispatch, id, account, relationship]);
|
}, [dispatch, id, account, relationship]);
|
||||||
|
|
||||||
const handleMuteNotifications = useCallback(() => {
|
const menu = useMemo(() => {
|
||||||
dispatch(muteAccount(id, true));
|
let arr: MenuItem[] = [];
|
||||||
}, [dispatch, id]);
|
|
||||||
|
|
||||||
const handleUnmuteNotifications = useCallback(() => {
|
if (defaultAction === 'mute') {
|
||||||
dispatch(muteAccount(id, false));
|
const handleMuteNotifications = () => {
|
||||||
}, [dispatch, id]);
|
dispatch(muteAccount(id, true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnmuteNotifications = () => {
|
||||||
|
dispatch(muteAccount(id, false));
|
||||||
|
};
|
||||||
|
|
||||||
|
arr = [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(
|
||||||
|
relationship?.muting_notifications
|
||||||
|
? messages.unmute_notifications
|
||||||
|
: messages.mute_notifications,
|
||||||
|
),
|
||||||
|
action: relationship?.muting_notifications
|
||||||
|
? handleUnmuteNotifications
|
||||||
|
: handleMuteNotifications,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else if (defaultAction !== 'block') {
|
||||||
|
const handleAddToLists = () => {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'LIST_ADDER',
|
||||||
|
modalProps: {
|
||||||
|
accountId: id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
arr = [
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(messages.addToLists),
|
||||||
|
action: handleAddToLists,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (accountUrl) {
|
||||||
|
arr.unshift(
|
||||||
|
{
|
||||||
|
text: intl.formatMessage(messages.openOriginalPage),
|
||||||
|
href: accountUrl,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
|
@ -94,68 +153,42 @@ export const Account: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let buttons;
|
let button: React.ReactNode, dropdown: React.ReactNode;
|
||||||
|
|
||||||
if (account && account.id !== me && relationship) {
|
if (menu.length > 0) {
|
||||||
const { requested, blocking, muting } = relationship;
|
dropdown = (
|
||||||
|
<Dropdown
|
||||||
if (requested) {
|
items={menu}
|
||||||
buttons = <FollowButton accountId={id} />;
|
icon='ellipsis-h'
|
||||||
} else if (blocking) {
|
iconComponent={MoreHorizIcon}
|
||||||
buttons = (
|
title={intl.formatMessage(messages.more)}
|
||||||
<Button
|
/>
|
||||||
text={intl.formatMessage(messages.unblock)}
|
);
|
||||||
onClick={handleBlock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else if (muting) {
|
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
text: intl.formatMessage(
|
|
||||||
relationship.muting_notifications
|
|
||||||
? messages.unmute_notifications
|
|
||||||
: messages.mute_notifications,
|
|
||||||
),
|
|
||||||
action: relationship.muting_notifications
|
|
||||||
? handleUnmuteNotifications
|
|
||||||
: handleMuteNotifications,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
buttons = (
|
|
||||||
<>
|
|
||||||
<Dropdown
|
|
||||||
items={menu}
|
|
||||||
icon='ellipsis-h'
|
|
||||||
iconComponent={MoreHorizIcon}
|
|
||||||
title={intl.formatMessage(messages.more)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
text={intl.formatMessage(messages.unmute)}
|
|
||||||
onClick={handleMute}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (defaultAction === 'mute') {
|
|
||||||
buttons = (
|
|
||||||
<Button text={intl.formatMessage(messages.mute)} onClick={handleMute} />
|
|
||||||
);
|
|
||||||
} else if (defaultAction === 'block') {
|
|
||||||
buttons = (
|
|
||||||
<Button
|
|
||||||
text={intl.formatMessage(messages.block)}
|
|
||||||
onClick={handleBlock}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
buttons = <FollowButton accountId={id} />;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
buttons = <FollowButton accountId={id} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let muteTimeRemaining;
|
if (defaultAction === 'block') {
|
||||||
|
button = (
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(
|
||||||
|
relationship?.blocking ? messages.unblock : messages.block,
|
||||||
|
)}
|
||||||
|
onClick={handleBlock}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (defaultAction === 'mute') {
|
||||||
|
button = (
|
||||||
|
<Button
|
||||||
|
text={intl.formatMessage(
|
||||||
|
relationship?.muting ? messages.unmute : messages.mute,
|
||||||
|
)}
|
||||||
|
onClick={handleMute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
button = <FollowButton accountId={id} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let muteTimeRemaining: React.ReactNode;
|
||||||
|
|
||||||
if (account?.mute_expires_at) {
|
if (account?.mute_expires_at) {
|
||||||
muteTimeRemaining = (
|
muteTimeRemaining = (
|
||||||
|
@ -165,7 +198,7 @@ export const Account: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let verification;
|
let verification: React.ReactNode;
|
||||||
|
|
||||||
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
|
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
|
||||||
|
|
||||||
|
@ -211,7 +244,12 @@ export const Account: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!minimal && <div className='account__relationship'>{buttons}</div>}
|
{!minimal && (
|
||||||
|
<div className='account__relationship'>
|
||||||
|
{dropdown}
|
||||||
|
{button}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{account &&
|
{account &&
|
||||||
|
|
|
@ -102,7 +102,7 @@ export interface HashtagProps {
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
history?: number[];
|
history?: number[];
|
||||||
name: string;
|
name: string;
|
||||||
people: number;
|
people?: number;
|
||||||
to: string;
|
to: string;
|
||||||
uses?: number;
|
uses?: number;
|
||||||
withGraph?: boolean;
|
withGraph?: boolean;
|
||||||
|
|
|
@ -1,25 +1,6 @@
|
||||||
import { Switch, Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
import AccountNavigation from 'mastodon/features/account/navigation';
|
|
||||||
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
import Trends from 'mastodon/features/getting_started/containers/trends_container';
|
||||||
import { showTrends } from 'mastodon/initial_state';
|
import { showTrends } from 'mastodon/initial_state';
|
||||||
|
|
||||||
const DefaultNavigation: React.FC = () => (showTrends ? <Trends /> : null);
|
|
||||||
|
|
||||||
export const NavigationPortal: React.FC = () => (
|
export const NavigationPortal: React.FC = () => (
|
||||||
<div className='navigation-panel__portal'>
|
<div className='navigation-panel__portal'>{showTrends && <Trends />}</div>
|
||||||
<Switch>
|
|
||||||
<Route path='/@:acct' exact component={AccountNavigation} />
|
|
||||||
<Route
|
|
||||||
path='/@:acct/tagged/:tagged?'
|
|
||||||
exact
|
|
||||||
component={AccountNavigation}
|
|
||||||
/>
|
|
||||||
<Route path='/@:acct/with_replies' exact component={AccountNavigation} />
|
|
||||||
<Route path='/@:acct/followers' exact component={AccountNavigation} />
|
|
||||||
<Route path='/@:acct/following' exact component={AccountNavigation} />
|
|
||||||
<Route path='/@:acct/media' exact component={AccountNavigation} />
|
|
||||||
<Route component={DefaultNavigation} />
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
|
43
app/javascript/mastodon/components/remote_hint.tsx
Normal file
43
app/javascript/mastodon/components/remote_hint.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { TimelineHint } from './timeline_hint';
|
||||||
|
|
||||||
|
interface RemoteHintProps {
|
||||||
|
accountId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RemoteHint: React.FC<RemoteHintProps> = ({ accountId }) => {
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
|
);
|
||||||
|
const domain = account?.acct ? account.acct.split('@')[1] : undefined;
|
||||||
|
if (
|
||||||
|
!account ||
|
||||||
|
!account.url ||
|
||||||
|
account.acct !== account.username ||
|
||||||
|
!domain
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimelineHint
|
||||||
|
url={account.url}
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.posts_may_be_missing'
|
||||||
|
defaultMessage='Some posts from this profile may be missing.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='hints.profiles.see_more_posts'
|
||||||
|
defaultMessage='See more posts on {domain}'
|
||||||
|
values={{ domain: <strong>{domain}</strong> }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,51 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import { Hashtag } from 'mastodon/components/hashtag';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
|
||||||
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' },
|
|
||||||
});
|
|
||||||
|
|
||||||
class FeaturedTags extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
account: ImmutablePropTypes.record,
|
|
||||||
featuredTags: ImmutablePropTypes.list,
|
|
||||||
tagged: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { account, featuredTags, intl } = this.props;
|
|
||||||
|
|
||||||
if (!account || account.get('suspended') || featuredTags.isEmpty()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='getting-started__trends'>
|
|
||||||
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4>
|
|
||||||
|
|
||||||
{featuredTags.take(3).map(featuredTag => (
|
|
||||||
<Hashtag
|
|
||||||
key={featuredTag.get('name')}
|
|
||||||
name={featuredTag.get('name')}
|
|
||||||
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`}
|
|
||||||
uses={featuredTag.get('statuses_count') * 1}
|
|
||||||
withGraph={false}
|
|
||||||
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(FeaturedTags);
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { List as ImmutableList } from 'immutable';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { makeGetAccount } from 'mastodon/selectors';
|
|
||||||
|
|
||||||
import FeaturedTags from '../components/featured_tags';
|
|
||||||
|
|
||||||
const mapStateToProps = () => {
|
|
||||||
const getAccount = makeGetAccount();
|
|
||||||
|
|
||||||
return (state, { accountId }) => ({
|
|
||||||
account: getAccount(state, accountId),
|
|
||||||
featuredTags: state.getIn(['user_lists', 'featured_tags', accountId, 'items'], ImmutableList()),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(FeaturedTags);
|
|
|
@ -1,52 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import FeaturedTags from 'mastodon/features/account/containers/featured_tags_container';
|
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { match: { params: { acct } } }) => {
|
|
||||||
const accountId = state.getIn(['accounts_map', normalizeForLookup(acct)]);
|
|
||||||
|
|
||||||
if (!accountId) {
|
|
||||||
return {
|
|
||||||
isLoading: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
accountId,
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
class AccountNavigation extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
match: PropTypes.shape({
|
|
||||||
params: PropTypes.shape({
|
|
||||||
acct: PropTypes.string,
|
|
||||||
tagged: PropTypes.string,
|
|
||||||
}).isRequired,
|
|
||||||
}).isRequired,
|
|
||||||
|
|
||||||
accountId: PropTypes.string,
|
|
||||||
isLoading: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { accountId, isLoading, match: { params: { tagged } } } = this.props;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FeaturedTags accountId={accountId} tagged={tagged} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(AccountNavigation);
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
|
||||||
|
|
||||||
|
interface EmptyMessageProps {
|
||||||
|
suspended: boolean;
|
||||||
|
hidden: boolean;
|
||||||
|
blockedBy: boolean;
|
||||||
|
accountId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||||
|
accountId,
|
||||||
|
suspended,
|
||||||
|
hidden,
|
||||||
|
blockedBy,
|
||||||
|
}) => {
|
||||||
|
if (!accountId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: React.ReactNode = null;
|
||||||
|
|
||||||
|
if (suspended) {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_suspended'
|
||||||
|
defaultMessage='Account suspended'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (hidden) {
|
||||||
|
message = <LimitedAccountHint accountId={accountId} />;
|
||||||
|
} else if (blockedBy) {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_unavailable'
|
||||||
|
defaultMessage='Profile unavailable'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='empty_column.account_featured'
|
||||||
|
defaultMessage='This list is empty'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className='empty-column-indicator'>{message}</div>;
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
|
||||||
|
import { Hashtag } from 'mastodon/components/hashtag';
|
||||||
|
|
||||||
|
export type TagMap = ImmutableMap<
|
||||||
|
'id' | 'name' | 'url' | 'statuses_count' | 'last_status_at' | 'accountId',
|
||||||
|
string | null
|
||||||
|
>;
|
||||||
|
|
||||||
|
interface FeaturedTagProps {
|
||||||
|
tag: TagMap;
|
||||||
|
account: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
lastStatusAt: {
|
||||||
|
id: 'account.featured_tags.last_status_at',
|
||||||
|
defaultMessage: 'Last post on {date}',
|
||||||
|
},
|
||||||
|
empty: {
|
||||||
|
id: 'account.featured_tags.last_status_never',
|
||||||
|
defaultMessage: 'No posts',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FeaturedTag: React.FC<FeaturedTagProps> = ({ tag, account }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const name = tag.get('name') ?? '';
|
||||||
|
const count = Number.parseInt(tag.get('statuses_count') ?? '');
|
||||||
|
return (
|
||||||
|
<Hashtag
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
to={`/@${account}/tagged/${name}`}
|
||||||
|
uses={count}
|
||||||
|
withGraph={false}
|
||||||
|
description={
|
||||||
|
count > 0
|
||||||
|
? intl.formatMessage(messages.lastStatusAt, {
|
||||||
|
date: intl.formatDate(tag.get('last_status_at') ?? '', {
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
: intl.formatMessage(messages.empty)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
156
app/javascript/mastodon/features/account_featured/index.tsx
Normal file
156
app/javascript/mastodon/features/account_featured/index.tsx
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
|
||||||
|
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
|
||||||
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
|
import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||||
|
import StatusContainer from 'mastodon/containers/status_container';
|
||||||
|
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
||||||
|
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { AccountHeader } from '../account_timeline/components/account_header';
|
||||||
|
import Column from '../ui/components/column';
|
||||||
|
|
||||||
|
import { EmptyMessage } from './components/empty_message';
|
||||||
|
import { FeaturedTag } from './components/featured_tag';
|
||||||
|
import type { TagMap } from './components/featured_tag';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
acct?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AccountFeatured = () => {
|
||||||
|
const accountId = useAccountId();
|
||||||
|
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
|
||||||
|
const forceEmptyState = suspended || blockedBy || hidden;
|
||||||
|
const { acct = '' } = useParams<Params>();
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) {
|
||||||
|
void dispatch(expandAccountFeaturedTimeline(accountId));
|
||||||
|
dispatch(fetchFeaturedTags(accountId));
|
||||||
|
}
|
||||||
|
}, [accountId, dispatch]);
|
||||||
|
|
||||||
|
const isLoading = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
!accountId ||
|
||||||
|
!!(state.timelines as ImmutableMap<string, unknown>).getIn([
|
||||||
|
`account:${accountId}:pinned`,
|
||||||
|
'isLoading',
|
||||||
|
]) ||
|
||||||
|
!!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
|
||||||
|
);
|
||||||
|
const featuredTags = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.user_lists.getIn(
|
||||||
|
['featured_tags', accountId, 'items'],
|
||||||
|
ImmutableList(),
|
||||||
|
) as ImmutableList<TagMap>,
|
||||||
|
);
|
||||||
|
const featuredStatusIds = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||||
|
[`account:${accountId}:pinned`, 'items'],
|
||||||
|
ImmutableList(),
|
||||||
|
) as ImmutableList<string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<AccountFeaturedWrapper accountId={accountId}>
|
||||||
|
<div className='scrollable__append'>
|
||||||
|
<LoadingIndicator />
|
||||||
|
</div>
|
||||||
|
</AccountFeaturedWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
|
||||||
|
return (
|
||||||
|
<AccountFeaturedWrapper accountId={accountId}>
|
||||||
|
<EmptyMessage
|
||||||
|
blockedBy={blockedBy}
|
||||||
|
hidden={hidden}
|
||||||
|
suspended={suspended}
|
||||||
|
accountId={accountId}
|
||||||
|
/>
|
||||||
|
<RemoteHint accountId={accountId} />
|
||||||
|
</AccountFeaturedWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<ColumnBackButton />
|
||||||
|
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
{accountId && (
|
||||||
|
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
|
||||||
|
)}
|
||||||
|
{!featuredTags.isEmpty() && (
|
||||||
|
<>
|
||||||
|
<h4 className='column-subheading'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.featured.hashtags'
|
||||||
|
defaultMessage='Hashtags'
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
{featuredTags.map((tag) => (
|
||||||
|
<FeaturedTag key={tag.get('id')} tag={tag} account={acct} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!featuredStatusIds.isEmpty() && (
|
||||||
|
<>
|
||||||
|
<h4 className='column-subheading'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='account.featured.posts'
|
||||||
|
defaultMessage='Posts'
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
{featuredStatusIds.map((statusId) => (
|
||||||
|
<StatusContainer
|
||||||
|
key={`f-${statusId}`}
|
||||||
|
// @ts-expect-error inferred props are wrong
|
||||||
|
id={statusId}
|
||||||
|
contextType='account'
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<RemoteHint accountId={accountId} />
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AccountFeaturedWrapper = ({
|
||||||
|
children,
|
||||||
|
accountId,
|
||||||
|
}: React.PropsWithChildren<{ accountId?: string }>) => {
|
||||||
|
return (
|
||||||
|
<Column>
|
||||||
|
<ColumnBackButton />
|
||||||
|
<div className='scrollable scrollable--flex'>
|
||||||
|
{accountId && <AccountHeader accountId={accountId} />}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
|
export default AccountFeatured;
|
|
@ -2,25 +2,22 @@ import { useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { createSelector } from '@reduxjs/toolkit';
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
|
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
|
||||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
|
import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
|
||||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||||
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
|
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
import { useAccountId } from 'mastodon/hooks/useAccountId';
|
||||||
|
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
|
||||||
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
import type { MediaAttachment } from 'mastodon/models/media_attachment';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
|
||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
|
||||||
import type { RootState } from 'mastodon/store';
|
import type { RootState } from 'mastodon/store';
|
||||||
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
import { useAppSelector, useAppDispatch } from 'mastodon/store';
|
||||||
|
|
||||||
|
@ -56,53 +53,11 @@ const getAccountGallery = createSelector(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interface Params {
|
|
||||||
acct?: string;
|
|
||||||
id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RemoteHint: React.FC<{
|
|
||||||
accountId: string;
|
|
||||||
}> = ({ accountId }) => {
|
|
||||||
const account = useAppSelector((state) => state.accounts.get(accountId));
|
|
||||||
const acct = account?.acct;
|
|
||||||
const url = account?.url;
|
|
||||||
const domain = acct ? acct.split('@')[1] : undefined;
|
|
||||||
|
|
||||||
if (!url) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimelineHint
|
|
||||||
url={url}
|
|
||||||
message={
|
|
||||||
<FormattedMessage
|
|
||||||
id='hints.profiles.posts_may_be_missing'
|
|
||||||
defaultMessage='Some posts from this profile may be missing.'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={
|
|
||||||
<FormattedMessage
|
|
||||||
id='hints.profiles.see_more_posts'
|
|
||||||
defaultMessage='See more posts on {domain}'
|
|
||||||
values={{ domain: <strong>{domain}</strong> }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AccountGallery: React.FC<{
|
export const AccountGallery: React.FC<{
|
||||||
multiColumn: boolean;
|
multiColumn: boolean;
|
||||||
}> = ({ multiColumn }) => {
|
}> = ({ multiColumn }) => {
|
||||||
const { acct, id } = useParams<Params>();
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const accountId = useAppSelector(
|
const accountId = useAccountId();
|
||||||
(state) =>
|
|
||||||
id ??
|
|
||||||
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
|
|
||||||
);
|
|
||||||
const attachments = useAppSelector((state) =>
|
const attachments = useAppSelector((state) =>
|
||||||
accountId
|
accountId
|
||||||
? getAccountGallery(state, accountId)
|
? getAccountGallery(state, accountId)
|
||||||
|
@ -123,33 +78,15 @@ export const AccountGallery: React.FC<{
|
||||||
const account = useAppSelector((state) =>
|
const account = useAppSelector((state) =>
|
||||||
accountId ? state.accounts.get(accountId) : undefined,
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
);
|
);
|
||||||
const blockedBy = useAppSelector(
|
|
||||||
(state) =>
|
|
||||||
state.relationships.getIn([accountId, 'blocked_by'], false) as boolean,
|
|
||||||
);
|
|
||||||
const suspended = useAppSelector(
|
|
||||||
(state) => state.accounts.getIn([accountId, 'suspended'], false) as boolean,
|
|
||||||
);
|
|
||||||
const isAccount = !!account;
|
const isAccount = !!account;
|
||||||
const remote = account?.acct !== account?.username;
|
|
||||||
const hidden = useAppSelector((state) =>
|
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
|
||||||
accountId ? getAccountHidden(state, accountId) : false,
|
|
||||||
);
|
|
||||||
const maxId = attachments.last()?.getIn(['status', 'id']) as
|
const maxId = attachments.last()?.getIn(['status', 'id']) as
|
||||||
| string
|
| string
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!accountId) {
|
|
||||||
dispatch(lookupAccount(acct));
|
|
||||||
}
|
|
||||||
}, [dispatch, accountId, acct]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (accountId && !isAccount) {
|
|
||||||
dispatch(fetchAccount(accountId));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountId && isAccount) {
|
if (accountId && isAccount) {
|
||||||
void dispatch(expandAccountMediaTimeline(accountId));
|
void dispatch(expandAccountMediaTimeline(accountId));
|
||||||
}
|
}
|
||||||
|
@ -233,7 +170,7 @@ export const AccountGallery: React.FC<{
|
||||||
defaultMessage='Profile unavailable'
|
defaultMessage='Profile unavailable'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (remote && attachments.isEmpty()) {
|
} else if (attachments.isEmpty()) {
|
||||||
emptyMessage = <RemoteHint accountId={accountId} />;
|
emptyMessage = <RemoteHint accountId={accountId} />;
|
||||||
} else {
|
} else {
|
||||||
emptyMessage = (
|
emptyMessage = (
|
||||||
|
@ -259,7 +196,7 @@ export const AccountGallery: React.FC<{
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remote && accountId && <RemoteHint accountId={accountId} />}
|
append={accountId && <RemoteHint accountId={accountId} />}
|
||||||
scrollKey='account_gallery'
|
scrollKey='account_gallery'
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
|
|
|
@ -956,6 +956,9 @@ export const AccountHeader: React.FC<{
|
||||||
|
|
||||||
{!(hideTabs || hidden) && (
|
{!(hideTabs || hidden) && (
|
||||||
<div className='account__section-headline'>
|
<div className='account__section-headline'>
|
||||||
|
<NavLink exact to={`/@${account.acct}/featured`}>
|
||||||
|
<FormattedMessage id='account.featured' defaultMessage='Featured' />
|
||||||
|
</NavLink>
|
||||||
<NavLink exact to={`/@${account.acct}`}>
|
<NavLink exact to={`/@${account.acct}`}>
|
||||||
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
<FormattedMessage id='account.posts' defaultMessage='Posts' />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
|
@ -7,12 +7,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { TimelineHint } from 'mastodon/components/timeline_hint';
|
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import { me } from 'mastodon/initial_state';
|
import { me } from 'mastodon/initial_state';
|
||||||
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
import { useAppSelector } from 'mastodon/store';
|
|
||||||
|
|
||||||
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
import { lookupAccount, fetchAccount } from '../../actions/accounts';
|
||||||
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
import { fetchFeaturedTags } from '../../actions/featured_tags';
|
||||||
|
@ -21,6 +19,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
|
||||||
import { LoadingIndicator } from '../../components/loading_indicator';
|
import { LoadingIndicator } from '../../components/loading_indicator';
|
||||||
import StatusList from '../../components/status_list';
|
import StatusList from '../../components/status_list';
|
||||||
import Column from '../ui/components/column';
|
import Column from '../ui/components/column';
|
||||||
|
import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||||
|
|
||||||
import { AccountHeader } from './components/account_header';
|
import { AccountHeader } from './components/account_header';
|
||||||
import { LimitedAccountHint } from './components/limited_account_hint';
|
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||||
|
@ -47,11 +46,8 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accountId,
|
accountId,
|
||||||
remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
|
|
||||||
remoteUrl: state.getIn(['accounts', accountId, 'url']),
|
|
||||||
isAccount: !!state.getIn(['accounts', accountId]),
|
isAccount: !!state.getIn(['accounts', accountId]),
|
||||||
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
|
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
|
||||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'], emptyList),
|
|
||||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||||
|
@ -60,24 +56,6 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const RemoteHint = ({ accountId, url }) => {
|
|
||||||
const acct = useAppSelector(state => state.accounts.get(accountId)?.acct);
|
|
||||||
const domain = acct ? acct.split('@')[1] : undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TimelineHint
|
|
||||||
url={url}
|
|
||||||
message={<FormattedMessage id='hints.profiles.posts_may_be_missing' defaultMessage='Some posts from this profile may be missing.' />}
|
|
||||||
label={<FormattedMessage id='hints.profiles.see_more_posts' defaultMessage='See more posts on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
RemoteHint.propTypes = {
|
|
||||||
url: PropTypes.string.isRequired,
|
|
||||||
accountId: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
class AccountTimeline extends ImmutablePureComponent {
|
class AccountTimeline extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -89,7 +67,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
accountId: PropTypes.string,
|
accountId: PropTypes.string,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
statusIds: ImmutablePropTypes.list,
|
statusIds: ImmutablePropTypes.list,
|
||||||
featuredStatusIds: ImmutablePropTypes.list,
|
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
withReplies: PropTypes.bool,
|
withReplies: PropTypes.bool,
|
||||||
|
@ -97,8 +74,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
isAccount: PropTypes.bool,
|
isAccount: PropTypes.bool,
|
||||||
suspended: PropTypes.bool,
|
suspended: PropTypes.bool,
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
remote: PropTypes.bool,
|
|
||||||
remoteUrl: PropTypes.string,
|
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -161,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
const { accountId, statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;
|
||||||
|
|
||||||
if (isLoading && statusIds.isEmpty()) {
|
if (isLoading && statusIds.isEmpty()) {
|
||||||
return (
|
return (
|
||||||
|
@ -191,8 +166,6 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
|
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column>
|
<Column>
|
||||||
<ColumnBackButton />
|
<ColumnBackButton />
|
||||||
|
@ -200,10 +173,9 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={remoteMessage}
|
append={<RemoteHint accountId={accountId} />}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
statusIds={forceEmptyState ? emptyList : statusIds}
|
statusIds={forceEmptyState ? emptyList : statusIds}
|
||||||
featuredStatusIds={featuredStatusIds}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={!forceEmptyState && hasMore}
|
hasMore={!forceEmptyState && hasMore}
|
||||||
onLoadMore={this.handleLoadMore}
|
onLoadMore={this.handleLoadMore}
|
||||||
|
|
|
@ -105,11 +105,10 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
|
||||||
|
|
||||||
<div className='notification-request__actions'>
|
<div className='notification-request__actions'>
|
||||||
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
<IconButton iconComponent={DeleteIcon} onClick={handleDismiss} title={intl.formatMessage(messages.dismiss)} />
|
||||||
<DropdownMenuContainer
|
<Dropdown
|
||||||
items={menu}
|
items={menu}
|
||||||
icons='ellipsis-h'
|
icon='ellipsis-h'
|
||||||
iconComponent={MoreHorizIcon}
|
iconComponent={MoreHorizIcon}
|
||||||
direction='right'
|
|
||||||
title={intl.formatMessage(messages.more)}
|
title={intl.formatMessage(messages.more)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -73,6 +73,7 @@ import {
|
||||||
About,
|
About,
|
||||||
PrivacyPolicy,
|
PrivacyPolicy,
|
||||||
TermsOfService,
|
TermsOfService,
|
||||||
|
AccountFeatured,
|
||||||
} from './util/async-components';
|
} from './util/async-components';
|
||||||
import { ColumnsContextProvider } from './util/columns_context';
|
import { ColumnsContextProvider } from './util/columns_context';
|
||||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||||
|
@ -236,6 +237,7 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
<WrappedRoute path={['/publish', '/statuses/new']} component={Compose} content={children} />
|
||||||
|
|
||||||
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
<WrappedRoute path={['/@:acct', '/accounts/:id']} exact component={AccountTimeline} content={children} />
|
||||||
|
<WrappedRoute path={['/@:acct/featured', '/accounts/:id/featured']} component={AccountFeatured} content={children} />
|
||||||
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
<WrappedRoute path='/@:acct/tagged/:tagged?' exact component={AccountTimeline} content={children} />
|
||||||
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
<WrappedRoute path={['/@:acct/with_replies', '/accounts/:id/with_replies']} component={AccountTimeline} content={children} componentParams={{ withReplies: true }} />
|
||||||
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
<WrappedRoute path={['/accounts/:id/followers', '/users/:acct/followers', '/@:acct/followers']} component={Followers} content={children} />
|
||||||
|
|
|
@ -66,6 +66,10 @@ export function AccountGallery () {
|
||||||
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
|
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function AccountFeatured() {
|
||||||
|
return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
|
||||||
|
}
|
||||||
|
|
||||||
export function Followers () {
|
export function Followers () {
|
||||||
return import(/* webpackChunkName: "features/followers" */'../../followers');
|
return import(/* webpackChunkName: "features/followers" */'../../followers');
|
||||||
}
|
}
|
||||||
|
|
37
app/javascript/mastodon/hooks/useAccountId.ts
Normal file
37
app/javascript/mastodon/hooks/useAccountId.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
|
import { fetchAccount, lookupAccount } from 'mastodon/actions/accounts';
|
||||||
|
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
interface Params {
|
||||||
|
acct?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAccountId() {
|
||||||
|
const { acct, id } = useParams<Params>();
|
||||||
|
const accountId = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
id ??
|
||||||
|
(state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
|
||||||
|
);
|
||||||
|
|
||||||
|
const account = useAppSelector((state) =>
|
||||||
|
accountId ? state.accounts.get(accountId) : undefined,
|
||||||
|
);
|
||||||
|
const isAccount = !!account;
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
if (!accountId) {
|
||||||
|
dispatch(lookupAccount(acct));
|
||||||
|
} else if (!isAccount) {
|
||||||
|
dispatch(fetchAccount(accountId));
|
||||||
|
}
|
||||||
|
}, [dispatch, accountId, acct, isAccount]);
|
||||||
|
|
||||||
|
return accountId;
|
||||||
|
}
|
20
app/javascript/mastodon/hooks/useAccountVisibility.ts
Normal file
20
app/javascript/mastodon/hooks/useAccountVisibility.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { getAccountHidden } from 'mastodon/selectors/accounts';
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
export function useAccountVisibility(accountId?: string) {
|
||||||
|
const blockedBy = useAppSelector(
|
||||||
|
(state) => !!state.relationships.getIn([accountId, 'blocked_by'], false),
|
||||||
|
);
|
||||||
|
const suspended = useAppSelector(
|
||||||
|
(state) => !!state.accounts.getIn([accountId, 'suspended'], false),
|
||||||
|
);
|
||||||
|
const hidden = useAppSelector((state) =>
|
||||||
|
accountId ? Boolean(getAccountHidden(state, accountId)) : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
blockedBy,
|
||||||
|
suspended,
|
||||||
|
hidden,
|
||||||
|
};
|
||||||
|
}
|
|
@ -27,9 +27,11 @@
|
||||||
"account.edit_profile": "Edit profile",
|
"account.edit_profile": "Edit profile",
|
||||||
"account.enable_notifications": "Notify me when @{name} posts",
|
"account.enable_notifications": "Notify me when @{name} posts",
|
||||||
"account.endorse": "Feature on profile",
|
"account.endorse": "Feature on profile",
|
||||||
|
"account.featured": "Featured",
|
||||||
|
"account.featured.hashtags": "Hashtags",
|
||||||
|
"account.featured.posts": "Posts",
|
||||||
"account.featured_tags.last_status_at": "Last post on {date}",
|
"account.featured_tags.last_status_at": "Last post on {date}",
|
||||||
"account.featured_tags.last_status_never": "No posts",
|
"account.featured_tags.last_status_never": "No posts",
|
||||||
"account.featured_tags.title": "{name}'s featured hashtags",
|
|
||||||
"account.follow": "Follow",
|
"account.follow": "Follow",
|
||||||
"account.follow_back": "Follow back",
|
"account.follow_back": "Follow back",
|
||||||
"account.followers": "Followers",
|
"account.followers": "Followers",
|
||||||
|
@ -294,6 +296,7 @@
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "Search results",
|
||||||
"emoji_button.symbols": "Symbols",
|
"emoji_button.symbols": "Symbols",
|
||||||
"emoji_button.travel": "Travel & Places",
|
"emoji_button.travel": "Travel & Places",
|
||||||
|
"empty_column.account_featured": "This list is empty",
|
||||||
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
||||||
"empty_column.account_suspended": "Account suspended",
|
"empty_column.account_suspended": "Account suspended",
|
||||||
"empty_column.account_timeline": "No posts here!",
|
"empty_column.account_timeline": "No posts here!",
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
"account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
|
"account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
|
||||||
"account.unblock": "رفع مسدودیت @{name}",
|
"account.unblock": "رفع مسدودیت @{name}",
|
||||||
"account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
|
"account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
|
||||||
|
"account.unblock_domain_short": "آنبلاک",
|
||||||
"account.unblock_short": "رفع مسدودیت",
|
"account.unblock_short": "رفع مسدودیت",
|
||||||
"account.unendorse": "معرّفی نکردن در نمایه",
|
"account.unendorse": "معرّفی نکردن در نمایه",
|
||||||
"account.unfollow": "پینگرفتن",
|
"account.unfollow": "پینگرفتن",
|
||||||
|
|
|
@ -65,6 +65,7 @@
|
||||||
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
|
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
|
||||||
"account.unblock": "Desbloquear @{name}",
|
"account.unblock": "Desbloquear @{name}",
|
||||||
"account.unblock_domain": "Desbloquear domínio {domain}",
|
"account.unblock_domain": "Desbloquear domínio {domain}",
|
||||||
|
"account.unblock_domain_short": "Desbloquear",
|
||||||
"account.unblock_short": "Desbloquear",
|
"account.unblock_short": "Desbloquear",
|
||||||
"account.unendorse": "Remover",
|
"account.unendorse": "Remover",
|
||||||
"account.unfollow": "Deixar de seguir",
|
"account.unfollow": "Deixar de seguir",
|
||||||
|
|
|
@ -45,7 +45,6 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
|
||||||
PreviewCard.sum(:image_file_size),
|
PreviewCard.sum(:image_file_size),
|
||||||
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
|
||||||
Backup.sum(:dump_file_size),
|
Backup.sum(:dump_file_size),
|
||||||
Import.sum(:data_file_size),
|
|
||||||
SiteUpload.sum(:file_file_size),
|
SiteUpload.sum(:file_file_size),
|
||||||
].sum
|
].sum
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,7 @@ class StatusCacheHydrator
|
||||||
|
|
||||||
def hydrate_non_reblog_payload(empty_payload, account_id)
|
def hydrate_non_reblog_payload(empty_payload, account_id)
|
||||||
empty_payload.tap do |payload|
|
empty_payload.tap do |payload|
|
||||||
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.id)
|
fill_status_payload(payload, @status, account_id)
|
||||||
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.id)
|
|
||||||
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.conversation_id)
|
|
||||||
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.id)
|
|
||||||
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.id) if @status.account_id == account_id
|
|
||||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status)
|
|
||||||
|
|
||||||
if payload[:poll]
|
if payload[:poll]
|
||||||
payload[:poll][:voted] = @status.account_id == account_id
|
payload[:poll][:voted] = @status.account_id == account_id
|
||||||
|
@ -45,18 +40,12 @@ class StatusCacheHydrator
|
||||||
payload[:muted] = false
|
payload[:muted] = false
|
||||||
payload[:bookmarked] = false
|
payload[:bookmarked] = false
|
||||||
payload[:pinned] = false if @status.account_id == account_id
|
payload[:pinned] = false if @status.account_id == account_id
|
||||||
payload[:filtered] = mapped_applied_custom_filter(account_id, @status.reblog)
|
|
||||||
|
|
||||||
# If the reblogged status is being delivered to the author who disabled the display of the application
|
# If the reblogged status is being delivered to the author who disabled the display of the application
|
||||||
# used to create the status, we need to hydrate it here too
|
# used to create the status, we need to hydrate it here too
|
||||||
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
|
payload[:reblog][:application] = payload_reblog_application if payload[:reblog][:application].nil? && @status.reblog.account_id == account_id
|
||||||
|
|
||||||
payload[:reblog][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
fill_status_payload(payload[:reblog], @status.reblog, account_id)
|
||||||
payload[:reblog][:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: @status.reblog_of_id)
|
|
||||||
payload[:reblog][:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: @status.reblog.conversation_id)
|
|
||||||
payload[:reblog][:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: @status.reblog_of_id)
|
|
||||||
payload[:reblog][:pinned] = StatusPin.exists?(account_id: account_id, status_id: @status.reblog_of_id) if @status.reblog.account_id == account_id
|
|
||||||
payload[:reblog][:filtered] = payload[:filtered]
|
|
||||||
|
|
||||||
if payload[:reblog][:poll]
|
if payload[:reblog][:poll]
|
||||||
if @status.reblog.account_id == account_id
|
if @status.reblog.account_id == account_id
|
||||||
|
@ -69,11 +58,21 @@ class StatusCacheHydrator
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
payload[:filtered] = payload[:reblog][:filtered]
|
||||||
payload[:favourited] = payload[:reblog][:favourited]
|
payload[:favourited] = payload[:reblog][:favourited]
|
||||||
payload[:reblogged] = payload[:reblog][:reblogged]
|
payload[:reblogged] = payload[:reblog][:reblogged]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fill_status_payload(payload, status, account_id)
|
||||||
|
payload[:favourited] = Favourite.exists?(account_id: account_id, status_id: status.id)
|
||||||
|
payload[:reblogged] = Status.exists?(account_id: account_id, reblog_of_id: status.id)
|
||||||
|
payload[:muted] = ConversationMute.exists?(account_id: account_id, conversation_id: status.conversation_id)
|
||||||
|
payload[:bookmarked] = Bookmark.exists?(account_id: account_id, status_id: status.id)
|
||||||
|
payload[:pinned] = StatusPin.exists?(account_id: account_id, status_id: status.id) if status.account_id == account_id
|
||||||
|
payload[:filtered] = mapped_applied_custom_filter(account_id, status)
|
||||||
|
end
|
||||||
|
|
||||||
def mapped_applied_custom_filter(account_id, status)
|
def mapped_applied_custom_filter(account_id, status)
|
||||||
CustomFilter
|
CustomFilter
|
||||||
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status)
|
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status)
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# == Schema Information
|
|
||||||
#
|
|
||||||
# Table name: imports
|
|
||||||
#
|
|
||||||
# id :bigint(8) not null, primary key
|
|
||||||
# type :integer not null
|
|
||||||
# approved :boolean default(FALSE), not null
|
|
||||||
# created_at :datetime not null
|
|
||||||
# updated_at :datetime not null
|
|
||||||
# data_file_name :string
|
|
||||||
# data_content_type :string
|
|
||||||
# data_file_size :integer
|
|
||||||
# data_updated_at :datetime
|
|
||||||
# account_id :bigint(8) not null
|
|
||||||
# overwrite :boolean default(FALSE), not null
|
|
||||||
#
|
|
||||||
|
|
||||||
# NOTE: This is a deprecated model, only kept to not break ongoing imports
|
|
||||||
# on upgrade. See `BulkImport` and `Form::Import` for its replacements.
|
|
||||||
|
|
||||||
class Import < ApplicationRecord
|
|
||||||
FILE_TYPES = %w(text/plain text/csv application/csv).freeze
|
|
||||||
MODES = %i(merge overwrite).freeze
|
|
||||||
|
|
||||||
self.inheritance_column = false
|
|
||||||
|
|
||||||
belongs_to :account
|
|
||||||
|
|
||||||
enum :type, { following: 0, blocking: 1, muting: 2, domain_blocking: 3, bookmarks: 4 }
|
|
||||||
|
|
||||||
validates :type, presence: true
|
|
||||||
|
|
||||||
has_attached_file :data
|
|
||||||
validates_attachment_content_type :data, content_type: FILE_TYPES
|
|
||||||
validates_attachment_presence :data
|
|
||||||
|
|
||||||
def mode
|
|
||||||
overwrite? ? :overwrite : :merge
|
|
||||||
end
|
|
||||||
|
|
||||||
def mode=(str)
|
|
||||||
self.overwrite = str.to_sym == :overwrite
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,144 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'csv'
|
|
||||||
|
|
||||||
# NOTE: This is a deprecated service, only kept to not break ongoing imports
|
|
||||||
# on upgrade. See `BulkImportService` for its replacement.
|
|
||||||
|
|
||||||
class ImportService < BaseService
|
|
||||||
ROWS_PROCESSING_LIMIT = 20_000
|
|
||||||
|
|
||||||
def call(import)
|
|
||||||
@import = import
|
|
||||||
@account = @import.account
|
|
||||||
|
|
||||||
case @import.type
|
|
||||||
when 'following'
|
|
||||||
import_follows!
|
|
||||||
when 'blocking'
|
|
||||||
import_blocks!
|
|
||||||
when 'muting'
|
|
||||||
import_mutes!
|
|
||||||
when 'domain_blocking'
|
|
||||||
import_domain_blocks!
|
|
||||||
when 'bookmarks'
|
|
||||||
import_bookmarks!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def import_follows!
|
|
||||||
parse_import_data!(['Account address'])
|
|
||||||
import_relationships!('follow', 'unfollow', @account.following, ROWS_PROCESSING_LIMIT, reblogs: { header: 'Show boosts', default: true }, notify: { header: 'Notify on new posts', default: false }, languages: { header: 'Languages', default: nil })
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_blocks!
|
|
||||||
parse_import_data!(['Account address'])
|
|
||||||
import_relationships!('block', 'unblock', @account.blocking, ROWS_PROCESSING_LIMIT)
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_mutes!
|
|
||||||
parse_import_data!(['Account address'])
|
|
||||||
import_relationships!('mute', 'unmute', @account.muting, ROWS_PROCESSING_LIMIT, notifications: { header: 'Hide notifications', default: true })
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_domain_blocks!
|
|
||||||
parse_import_data!(['#domain'])
|
|
||||||
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#domain'].strip }
|
|
||||||
|
|
||||||
if @import.overwrite?
|
|
||||||
presence_hash = items.index_with(true)
|
|
||||||
|
|
||||||
@account.domain_blocks.find_each do |domain_block|
|
|
||||||
if presence_hash[domain_block.domain]
|
|
||||||
items.delete(domain_block.domain)
|
|
||||||
else
|
|
||||||
@account.unblock_domain!(domain_block.domain)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
items.each do |domain|
|
|
||||||
@account.block_domain!(domain)
|
|
||||||
end
|
|
||||||
|
|
||||||
AfterAccountDomainBlockWorker.push_bulk(items) do |domain|
|
|
||||||
[@account.id, domain]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_relationships!(action, undo_action, overwrite_scope, limit, extra_fields = {})
|
|
||||||
local_domain_suffix = "@#{Rails.configuration.x.local_domain}"
|
|
||||||
items = @data.take(limit).map { |row| [row['Account address']&.strip&.delete_suffix(local_domain_suffix), extra_fields.to_h { |key, field_settings| [key, row[field_settings[:header]]&.strip || field_settings[:default]] }] }.reject { |(id, _)| id.blank? }
|
|
||||||
|
|
||||||
if @import.overwrite?
|
|
||||||
presence_hash = items.each_with_object({}) { |(id, extra), mapping| mapping[id] = [true, extra] }
|
|
||||||
|
|
||||||
overwrite_scope.reorder(nil).find_each do |target_account|
|
|
||||||
if presence_hash[target_account.acct]
|
|
||||||
items.delete(target_account.acct)
|
|
||||||
extra = presence_hash[target_account.acct][1]
|
|
||||||
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, action, extra.stringify_keys)
|
|
||||||
else
|
|
||||||
Import::RelationshipWorker.perform_async(@account.id, target_account.acct, undo_action)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
head_items = items.uniq { |acct, _| acct.split('@')[1] }
|
|
||||||
tail_items = items - head_items
|
|
||||||
|
|
||||||
Import::RelationshipWorker.push_bulk(head_items + tail_items) do |acct, extra|
|
|
||||||
[@account.id, acct, action, extra.stringify_keys]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_bookmarks!
|
|
||||||
parse_import_data!(['#uri'])
|
|
||||||
items = @data.take(ROWS_PROCESSING_LIMIT).map { |row| row['#uri'].strip }
|
|
||||||
|
|
||||||
if @import.overwrite?
|
|
||||||
presence_hash = items.index_with(true)
|
|
||||||
|
|
||||||
@account.bookmarks.find_each do |bookmark|
|
|
||||||
if presence_hash[bookmark.status.uri]
|
|
||||||
items.delete(bookmark.status.uri)
|
|
||||||
else
|
|
||||||
bookmark.destroy!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
statuses = items.filter_map do |uri|
|
|
||||||
status = ActivityPub::TagManager.instance.uri_to_resource(uri, Status)
|
|
||||||
next if status.nil? && ActivityPub::TagManager.instance.local_uri?(uri)
|
|
||||||
|
|
||||||
status || ActivityPub::FetchRemoteStatusService.new.call(uri)
|
|
||||||
rescue *Mastodon::HTTP_CONNECTION_ERRORS, Mastodon::UnexpectedResponseError
|
|
||||||
nil
|
|
||||||
rescue => e
|
|
||||||
Rails.logger.warn "Unexpected error when importing bookmark: #{e}"
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
|
|
||||||
account_ids = statuses.map(&:account_id)
|
|
||||||
preloaded_relations = @account.relations_map(account_ids, skip_blocking_and_muting: true)
|
|
||||||
|
|
||||||
statuses.keep_if { |status| StatusPolicy.new(@account, status, preloaded_relations).show? }
|
|
||||||
|
|
||||||
statuses.each do |status|
|
|
||||||
@account.bookmarks.find_or_create_by!(account: @account, status: status)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_import_data!(default_headers)
|
|
||||||
data = CSV.parse(import_data, headers: true)
|
|
||||||
data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(' ')
|
|
||||||
@data = data.compact_blank
|
|
||||||
end
|
|
||||||
|
|
||||||
def import_data
|
|
||||||
Paperclip.io_adapters.for(@import.data).read.force_encoding(Encoding::UTF_8)
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,57 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# NOTE: This is a deprecated worker, only kept to not break ongoing imports
|
|
||||||
# on upgrade. See `Import::RowWorker` for its replacement.
|
|
||||||
|
|
||||||
class Import::RelationshipWorker
|
|
||||||
include Sidekiq::Worker
|
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: 8, dead: false
|
|
||||||
|
|
||||||
def perform(account_id, target_account_uri, relationship, options)
|
|
||||||
from_account = Account.find(account_id)
|
|
||||||
target_domain = domain(target_account_uri)
|
|
||||||
target_account = stoplight_wrapper(target_domain).run { ResolveAccountService.new.call(target_account_uri, { check_delivery_availability: true }) }
|
|
||||||
options.symbolize_keys!
|
|
||||||
|
|
||||||
return if target_account.nil?
|
|
||||||
|
|
||||||
case relationship
|
|
||||||
when 'follow'
|
|
||||||
begin
|
|
||||||
FollowService.new.call(from_account, target_account, **options)
|
|
||||||
rescue ActiveRecord::RecordInvalid
|
|
||||||
raise if FollowLimitValidator.limit_for_account(from_account) < from_account.following_count
|
|
||||||
end
|
|
||||||
when 'unfollow'
|
|
||||||
UnfollowService.new.call(from_account, target_account)
|
|
||||||
when 'block'
|
|
||||||
BlockService.new.call(from_account, target_account)
|
|
||||||
when 'unblock'
|
|
||||||
UnblockService.new.call(from_account, target_account)
|
|
||||||
when 'mute'
|
|
||||||
MuteService.new.call(from_account, target_account, **options)
|
|
||||||
when 'unmute'
|
|
||||||
UnmuteService.new.call(from_account, target_account)
|
|
||||||
end
|
|
||||||
rescue ActiveRecord::RecordNotFound
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def domain(uri)
|
|
||||||
domain = uri.is_a?(Account) ? uri.domain : uri.split('@')[1]
|
|
||||||
TagManager.instance.local_domain?(domain) ? nil : TagManager.instance.normalize_domain(domain)
|
|
||||||
end
|
|
||||||
|
|
||||||
def stoplight_wrapper(domain)
|
|
||||||
if domain.present?
|
|
||||||
Stoplight("source:#{domain}")
|
|
||||||
.with_fallback { nil }
|
|
||||||
.with_threshold(1)
|
|
||||||
.with_cool_off_time(5.minutes.seconds)
|
|
||||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) || error.is_a?(OpenSSL::SSL::SSLError) ? handle.call(error) : raise(error) }
|
|
||||||
else
|
|
||||||
Stoplight('domain-blank')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,17 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# NOTE: This is a deprecated worker, only kept to not break ongoing imports
|
|
||||||
# on upgrade. See `ImportWorker` for its replacement.
|
|
||||||
|
|
||||||
class ImportWorker
|
|
||||||
include Sidekiq::Worker
|
|
||||||
|
|
||||||
sidekiq_options queue: 'pull', retry: false
|
|
||||||
|
|
||||||
def perform(import_id)
|
|
||||||
import = Import.find(import_id)
|
|
||||||
ImportService.new.call(import)
|
|
||||||
ensure
|
|
||||||
import&.destroy
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -169,7 +169,7 @@ else
|
||||||
end
|
end
|
||||||
|
|
||||||
Rails.application.reloader.to_prepare do
|
Rails.application.reloader.to_prepare do
|
||||||
Paperclip.options[:content_type_mappings] = { csv: Import::FILE_TYPES }
|
Paperclip.options[:content_type_mappings] = { csv: %w(text/plain text/csv application/csv) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# In some places in the code, we rescue this exception, but we don't always
|
# In some places in the code, we rescue this exception, but we don't always
|
||||||
|
|
|
@ -479,6 +479,22 @@ fa:
|
||||||
new:
|
new:
|
||||||
title: درونریزی انسدادهای دامنه
|
title: درونریزی انسدادهای دامنه
|
||||||
no_file: هیچ پروندهای گزیده نشده
|
no_file: هیچ پروندهای گزیده نشده
|
||||||
|
fasp:
|
||||||
|
debug:
|
||||||
|
callbacks:
|
||||||
|
delete: حذف
|
||||||
|
providers:
|
||||||
|
active: فعال
|
||||||
|
delete: حذف
|
||||||
|
finish_registration: تکمیل ثبتنام
|
||||||
|
name: نام
|
||||||
|
providers: ارائه دهندگان
|
||||||
|
registrations:
|
||||||
|
confirm: تایید
|
||||||
|
reject: رد کردن
|
||||||
|
save: ذخیره
|
||||||
|
sign_in: ورود
|
||||||
|
status: وضعیت
|
||||||
follow_recommendations:
|
follow_recommendations:
|
||||||
description_html: "<strong>پیشنهادات پیگیری به کاربران جدید کک میکند تا سریعتر محتوای جالب را پیدا کنند</strong>. زمانی که کاربری هنوز به اندازه کافی با دیگران تعامل نداشته است تا پیشنهادات پیگیری شخصیسازیشده دریافت کند، این حسابها را به جای آن فهرست مشاهده خواهد کرد. این حسابها به صورت روزانه و در ترکیب با بیشتری تعاملات و بالاترین دنبالکنندگان محلی برای یک زبان مشخص بازمحاسبه میشوند."
|
description_html: "<strong>پیشنهادات پیگیری به کاربران جدید کک میکند تا سریعتر محتوای جالب را پیدا کنند</strong>. زمانی که کاربری هنوز به اندازه کافی با دیگران تعامل نداشته است تا پیشنهادات پیگیری شخصیسازیشده دریافت کند، این حسابها را به جای آن فهرست مشاهده خواهد کرد. این حسابها به صورت روزانه و در ترکیب با بیشتری تعاملات و بالاترین دنبالکنندگان محلی برای یک زبان مشخص بازمحاسبه میشوند."
|
||||||
language: برای زبان
|
language: برای زبان
|
||||||
|
|
|
@ -259,6 +259,7 @@ lv:
|
||||||
create_user_role_html: "%{name} nomainīja %{target} lomu"
|
create_user_role_html: "%{name} nomainīja %{target} lomu"
|
||||||
demote_user_html: "%{name} pazemināja lietotāju %{target}"
|
demote_user_html: "%{name} pazemināja lietotāju %{target}"
|
||||||
destroy_announcement_html: "%{name} izdzēsa paziņojumu %{target}"
|
destroy_announcement_html: "%{name} izdzēsa paziņojumu %{target}"
|
||||||
|
destroy_canonical_email_block_html: "%{name} atcēla e-pasta adreses liegumu ar jaucējvērtību %{target}"
|
||||||
destroy_custom_emoji_html: "%{name} izdzēsa emocijzīmi %{target}"
|
destroy_custom_emoji_html: "%{name} izdzēsa emocijzīmi %{target}"
|
||||||
destroy_domain_allow_html: "%{name} neatļāva federāciju ar domēnu %{target}"
|
destroy_domain_allow_html: "%{name} neatļāva federāciju ar domēnu %{target}"
|
||||||
destroy_domain_block_html: "%{name} atbloķēja domēnu %{target}"
|
destroy_domain_block_html: "%{name} atbloķēja domēnu %{target}"
|
||||||
|
@ -409,7 +410,7 @@ lv:
|
||||||
permanent_action: Apturēšanas atsaukšana neatjaunos nekādus datus vai attiecības.
|
permanent_action: Apturēšanas atsaukšana neatjaunos nekādus datus vai attiecības.
|
||||||
preamble_html: Tu gatavojies apturēt domēna <strong>%{domain}</strong> un tā apakšdomēnu darbību.
|
preamble_html: Tu gatavojies apturēt domēna <strong>%{domain}</strong> un tā apakšdomēnu darbību.
|
||||||
remove_all_data: Tādējādi no tava servera tiks noņemts viss šī domēna kontu saturs, multivide un profila dati.
|
remove_all_data: Tādējādi no tava servera tiks noņemts viss šī domēna kontu saturs, multivide un profila dati.
|
||||||
stop_communication: Jūsu serveris pārtrauks sazināties ar šiem serveriem.
|
stop_communication: Tavs serveris pārtrauks sazināties ar šiem serveriem.
|
||||||
title: Apstiprināt domēna %{domain} bloķēšanu
|
title: Apstiprināt domēna %{domain} bloķēšanu
|
||||||
undo_relationships: Tādējādi tiks atsauktas jebkuras sekošanas attiecības starp šo un tavu serveru kontiem.
|
undo_relationships: Tādējādi tiks atsauktas jebkuras sekošanas attiecības starp šo un tavu serveru kontiem.
|
||||||
created_msg: Domēna bloķēšana tagad tiek apstrādāta
|
created_msg: Domēna bloķēšana tagad tiek apstrādāta
|
||||||
|
@ -949,11 +950,13 @@ lv:
|
||||||
message_html: "<strong>Tava objektu krātuve ir nepareizi konfigurēta. Tavu lietotāju privātums ir apdraudēts.</strong>"
|
message_html: "<strong>Tava objektu krātuve ir nepareizi konfigurēta. Tavu lietotāju privātums ir apdraudēts.</strong>"
|
||||||
tags:
|
tags:
|
||||||
moderation:
|
moderation:
|
||||||
|
not_trendable: Nav izplatīts
|
||||||
not_usable: Nav izmantojams
|
not_usable: Nav izmantojams
|
||||||
pending_review: Gaida pārskatīšanu
|
pending_review: Gaida pārskatīšanu
|
||||||
review_requested: Pieprasīta pārskatīšana
|
review_requested: Pieprasīta pārskatīšana
|
||||||
reviewed: Pārskatīts
|
reviewed: Pārskatīts
|
||||||
title: Stāvoklis
|
title: Stāvoklis
|
||||||
|
trendable: Izplatīts
|
||||||
unreviewed: Nepārskatīts
|
unreviewed: Nepārskatīts
|
||||||
usable: Izmantojams
|
usable: Izmantojams
|
||||||
name: Nosaukums
|
name: Nosaukums
|
||||||
|
@ -1312,7 +1315,7 @@ lv:
|
||||||
appeal_approved: Šis brīdinājums tika sekmīgi pārsūdzēts un vairs nav spēkā
|
appeal_approved: Šis brīdinājums tika sekmīgi pārsūdzēts un vairs nav spēkā
|
||||||
appeal_rejected: Apelācija ir noraidīta
|
appeal_rejected: Apelācija ir noraidīta
|
||||||
appeal_submitted_at: Apelācija iesniegta
|
appeal_submitted_at: Apelācija iesniegta
|
||||||
appealed_msg: Jūsu apelācija ir iesniegta. Ja tā tiks apstiprināta, jums tiks paziņots.
|
appealed_msg: Tava pārsūdzība ir iesniegta. Ja tā tiks apstiprināta, Tev tiks paziņots.
|
||||||
appeals:
|
appeals:
|
||||||
submit: Iesniegt apelāciju
|
submit: Iesniegt apelāciju
|
||||||
approve_appeal: Apstiprināt apelāciju
|
approve_appeal: Apstiprināt apelāciju
|
||||||
|
@ -1332,9 +1335,9 @@ lv:
|
||||||
sensitive: Konta atzīmēšana kā jūtīgu
|
sensitive: Konta atzīmēšana kā jūtīgu
|
||||||
silence: Konta ierobežošana
|
silence: Konta ierobežošana
|
||||||
suspend: Konta apturēšana
|
suspend: Konta apturēšana
|
||||||
your_appeal_approved: Jūsu apelācija ir apstiprināta
|
your_appeal_approved: Tava pārsūdzība tika apstiprināta
|
||||||
your_appeal_pending: Jūs esat iesniedzis apelāciju
|
your_appeal_pending: Jūs esat iesniedzis apelāciju
|
||||||
your_appeal_rejected: Jūsu apelācija ir noraidīta
|
your_appeal_rejected: Tava pārsūdzība tika noraidīta
|
||||||
edit_profile:
|
edit_profile:
|
||||||
basic_information: Pamata informācija
|
basic_information: Pamata informācija
|
||||||
hint_html: "<strong>Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem.</strong> Ir lielāka iespējamība, ka citi clivēki sekos Tev un mijiedarbosies ar Tevi, ja Tev ir aizpildīts profils un profila attēls."
|
hint_html: "<strong>Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem.</strong> Ir lielāka iespējamība, ka citi clivēki sekos Tev un mijiedarbosies ar Tevi, ja Tev ir aizpildīts profils un profila attēls."
|
||||||
|
@ -1929,7 +1932,7 @@ lv:
|
||||||
'7889238': 3 mēneši
|
'7889238': 3 mēneši
|
||||||
min_age_label: Vecuma slieksnis
|
min_age_label: Vecuma slieksnis
|
||||||
min_favs: Saglabāt ziņas izlsasē vismaz
|
min_favs: Saglabāt ziņas izlsasē vismaz
|
||||||
min_favs_hint: Nedzēš nevienu jūsu ziņu, kas ir saņēmusi vismaz tik daudz izcēlumu. Atstājiet tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
|
min_favs_hint: Neizdzēš nevienu no Taviem ierakstiem, kas ir pievienoti šādā daudzumā izlašu. Atstāt tukšu, lai izdzēstu ierakstus neatkarīgi no tā, cik izlasēs tie ir pievienoti
|
||||||
min_reblogs: Saglabāt ziņas izceltas vismaz
|
min_reblogs: Saglabāt ziņas izceltas vismaz
|
||||||
min_reblogs_hint: Neizdzēš nevienu no tavām ziņām, kas ir izceltas vismaz tik reižu. Atstāj tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
|
min_reblogs_hint: Neizdzēš nevienu no tavām ziņām, kas ir izceltas vismaz tik reižu. Atstāj tukšu, lai dzēstu ziņas neatkarīgi no to izcēlumu skaita
|
||||||
stream_entries:
|
stream_entries:
|
||||||
|
@ -1978,12 +1981,13 @@ lv:
|
||||||
title: "%{domain} pakalpojuma paziņojums"
|
title: "%{domain} pakalpojuma paziņojums"
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Konta iestatījumi
|
action: Konta iestatījumi
|
||||||
explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir apstiprināta. Jūsu konts atkal ir labā stāvoklī.
|
explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, ir apstiprināta. Tavs konts atkal ir labā stāvoklī.
|
||||||
subject: Jūsu %{date} apelācija ir apstiprināta
|
subject: Tava %{date} iesniegtā pārsūdzība tika apstiprināta
|
||||||
title: Apelācija apstiprināta
|
title: Apelācija apstiprināta
|
||||||
appeal_rejected:
|
appeal_rejected:
|
||||||
explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir noraidīta.
|
explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, tika noraidīta.
|
||||||
subject: Jūsu %{date} apelācija ir noraidīta
|
subject: Tava %{date} iesniegta pārsūdzība tika noraidīta
|
||||||
|
subtitle: Tava pārsūdzība tika noraidīta.
|
||||||
title: Apelācija noraidīta
|
title: Apelācija noraidīta
|
||||||
backup_ready:
|
backup_ready:
|
||||||
explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju.
|
explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju.
|
||||||
|
@ -1993,6 +1997,9 @@ lv:
|
||||||
failed_2fa:
|
failed_2fa:
|
||||||
details: 'Šeit ir informācija par pieteikšanās mēģinājumu:'
|
details: 'Šeit ir informācija par pieteikšanās mēģinājumu:'
|
||||||
explanation: Kāds mēģināja pieteikties Tavā kontā, bet norādīja nederīgu otro autentificēšanās soli.
|
explanation: Kāds mēģināja pieteikties Tavā kontā, bet norādīja nederīgu otro autentificēšanās soli.
|
||||||
|
further_actions_html: Ja tas nebiji Tu, mēs iesakām nekavējoties %{action}, jo var būt noticis drošības pārkāpums.
|
||||||
|
subject: Otrās pakāpes autentificēšanās atteice
|
||||||
|
title: Neizdevās otrās pakāpes autentificēšanās
|
||||||
suspicious_sign_in:
|
suspicious_sign_in:
|
||||||
change_password: mainīt paroli
|
change_password: mainīt paroli
|
||||||
details: 'Šeit ir pieteikšanās izvērsums:'
|
details: 'Šeit ir pieteikšanās izvērsums:'
|
||||||
|
|
|
@ -320,6 +320,7 @@ pt-BR:
|
||||||
title: Novo anúncio
|
title: Novo anúncio
|
||||||
preview:
|
preview:
|
||||||
explanation_html: 'Esse e-mail será enviado a <strong>%{display_count} usuários</strong>. O texto a seguir será incluído ao e-mail:'
|
explanation_html: 'Esse e-mail será enviado a <strong>%{display_count} usuários</strong>. O texto a seguir será incluído ao e-mail:'
|
||||||
|
title: Visualizar anúncio
|
||||||
publish: Publicar
|
publish: Publicar
|
||||||
published_msg: Anúncio publicado!
|
published_msg: Anúncio publicado!
|
||||||
scheduled_for: Agendado para %{time}
|
scheduled_for: Agendado para %{time}
|
||||||
|
@ -484,19 +485,30 @@ pt-BR:
|
||||||
created_at: Criado em
|
created_at: Criado em
|
||||||
delete: Apagar
|
delete: Apagar
|
||||||
ip: Endereço de IP
|
ip: Endereço de IP
|
||||||
|
request_body: Corpo da solicitação
|
||||||
|
title: Depurar Callbacks
|
||||||
providers:
|
providers:
|
||||||
|
active: Ativo
|
||||||
base_url: URL Base
|
base_url: URL Base
|
||||||
|
callback: Callback
|
||||||
delete: Apagar
|
delete: Apagar
|
||||||
|
edit: Editar provedor
|
||||||
finish_registration: Finalizar o cadastro
|
finish_registration: Finalizar o cadastro
|
||||||
name: Nome
|
name: Nome
|
||||||
|
providers: Provedores
|
||||||
public_key_fingerprint: Impressão digital de chave pública
|
public_key_fingerprint: Impressão digital de chave pública
|
||||||
registration_requested: Cadastro solicitado
|
registration_requested: Cadastro solicitado
|
||||||
registrations:
|
registrations:
|
||||||
confirm: Confirmar
|
confirm: Confirmar
|
||||||
|
description: Você recebeu um registro de um FASP. Rejeite se você não tiver iniciado isso. Se você iniciou isso, compare cuidadosamente o nome e a impressão digital da chave antes de confirmar o registro.
|
||||||
reject: Rejeitar
|
reject: Rejeitar
|
||||||
|
title: Confirmar o registro FASP
|
||||||
save: Salvar
|
save: Salvar
|
||||||
|
select_capabilities: Selecionar recursos
|
||||||
sign_in: Entrar
|
sign_in: Entrar
|
||||||
status: Estado
|
status: Estado
|
||||||
|
title: Provedores de serviços auxiliares do Fediverso
|
||||||
|
title: FASP
|
||||||
follow_recommendations:
|
follow_recommendations:
|
||||||
description_html: "<strong>A recomendação de contas ajuda os novos usuários a encontrar rapidamente conteúdo interessante</strong>. Quando um usuário ainda não tiver interagido o suficiente para gerar recomendações de contas, essas contas serão recomendadas. Essas recomendações são recalculadas diariamente a partir de uma lista de contas com alto engajamento e maior número de seguidores locais em uma dada língua."
|
description_html: "<strong>A recomendação de contas ajuda os novos usuários a encontrar rapidamente conteúdo interessante</strong>. Quando um usuário ainda não tiver interagido o suficiente para gerar recomendações de contas, essas contas serão recomendadas. Essas recomendações são recalculadas diariamente a partir de uma lista de contas com alto engajamento e maior número de seguidores locais em uma dada língua."
|
||||||
language: Na língua
|
language: Na língua
|
||||||
|
@ -1927,6 +1939,10 @@ pt-BR:
|
||||||
recovery_instructions_html: Se você perder acesso ao seu celular, você pode usar um dos códigos de recuperação abaixo para acessar a sua conta. <strong>Mantenha os códigos de recuperação em um local seguro</strong>. Por exemplo, você pode imprimi-los e guardá-los junto a outros documentos importantes.
|
recovery_instructions_html: Se você perder acesso ao seu celular, você pode usar um dos códigos de recuperação abaixo para acessar a sua conta. <strong>Mantenha os códigos de recuperação em um local seguro</strong>. Por exemplo, você pode imprimi-los e guardá-los junto a outros documentos importantes.
|
||||||
webauthn: Chaves de segurança
|
webauthn: Chaves de segurança
|
||||||
user_mailer:
|
user_mailer:
|
||||||
|
announcement_published:
|
||||||
|
description: 'Os administradores do %{domain} estão fazendo um anúncio:'
|
||||||
|
subject: Anúncio de serviço
|
||||||
|
title: Anúncio de serviço de %{domain}
|
||||||
appeal_approved:
|
appeal_approved:
|
||||||
action: Configurações da conta
|
action: Configurações da conta
|
||||||
explanation: A revisão da punição na sua conta em %{strike_date} que você enviou em %{appeal_date} foi aprovada. Sua conta está novamente em situação regular.
|
explanation: A revisão da punição na sua conta em %{strike_date} que você enviou em %{appeal_date} foi aprovada. Sua conta está novamente em situação regular.
|
||||||
|
|
|
@ -349,6 +349,9 @@ fa:
|
||||||
jurisdiction: صلاحیت قانونی
|
jurisdiction: صلاحیت قانونی
|
||||||
min_age: کمینهٔ زمان
|
min_age: کمینهٔ زمان
|
||||||
user:
|
user:
|
||||||
|
date_of_birth_1i: روز
|
||||||
|
date_of_birth_2i: ماه
|
||||||
|
date_of_birth_3i: سال
|
||||||
role: نقش
|
role: نقش
|
||||||
time_zone: منطقهٔ زمانی
|
time_zone: منطقهٔ زمانی
|
||||||
user_role:
|
user_role:
|
||||||
|
|
|
@ -129,6 +129,7 @@ Rails.application.routes.draw do
|
||||||
constraints(username: %r{[^@/.]+}) do
|
constraints(username: %r{[^@/.]+}) do
|
||||||
with_options to: 'accounts#show' do
|
with_options to: 'accounts#show' do
|
||||||
get '/@:username', as: :short_account
|
get '/@:username', as: :short_account
|
||||||
|
get '/@:username/featured'
|
||||||
get '/@:username/with_replies', as: :short_account_with_replies
|
get '/@:username/with_replies', as: :short_account_with_replies
|
||||||
get '/@:username/media', as: :short_account_media
|
get '/@:username/media', as: :short_account_media
|
||||||
get '/@:username/tagged/:tag', as: :short_account_tag
|
get '/@:username/tagged/:tag', as: :short_account_tag
|
||||||
|
|
11
db/migrate/20250410144908_drop_imports.rb
Normal file
11
db/migrate/20250410144908_drop_imports.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DropImports < ActiveRecord::Migration[7.1]
|
||||||
|
def up
|
||||||
|
drop_table :imports
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
raise ActiveRecord::IrreversibleMigration
|
||||||
|
end
|
||||||
|
end
|
16
db/schema.rb
16
db/schema.rb
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
ActiveRecord::Schema[8.0].define(version: 2025_04_10_144908) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pg_catalog.plpgsql"
|
enable_extension "pg_catalog.plpgsql"
|
||||||
|
|
||||||
|
@ -555,19 +555,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
||||||
t.index ["user_id"], name: "index_identities_on_user_id"
|
t.index ["user_id"], name: "index_identities_on_user_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "imports", force: :cascade do |t|
|
|
||||||
t.integer "type", null: false
|
|
||||||
t.boolean "approved", default: false, null: false
|
|
||||||
t.datetime "created_at", precision: nil, null: false
|
|
||||||
t.datetime "updated_at", precision: nil, null: false
|
|
||||||
t.string "data_file_name"
|
|
||||||
t.string "data_content_type"
|
|
||||||
t.integer "data_file_size"
|
|
||||||
t.datetime "data_updated_at", precision: nil
|
|
||||||
t.bigint "account_id", null: false
|
|
||||||
t.boolean "overwrite", default: false, null: false
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "invites", force: :cascade do |t|
|
create_table "invites", force: :cascade do |t|
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.string "code", default: "", null: false
|
t.string "code", default: "", null: false
|
||||||
|
@ -1329,7 +1316,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_13_123400) do
|
||||||
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade
|
||||||
add_foreign_key "generated_annual_reports", "accounts"
|
add_foreign_key "generated_annual_reports", "accounts"
|
||||||
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
add_foreign_key "identities", "users", name: "fk_bea040f377", on_delete: :cascade
|
||||||
add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade
|
|
||||||
add_foreign_key "invites", "users", on_delete: :cascade
|
add_foreign_key "invites", "users", on_delete: :cascade
|
||||||
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
add_foreign_key "list_accounts", "accounts", on_delete: :cascade
|
||||||
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
|
||||||
|
|
|
@ -293,7 +293,6 @@ module Mastodon::CLI
|
||||||
Account
|
Account
|
||||||
Backup
|
Backup
|
||||||
CustomEmoji
|
CustomEmoji
|
||||||
Import
|
|
||||||
MediaAttachment
|
MediaAttachment
|
||||||
PreviewCard
|
PreviewCard
|
||||||
SiteUpload
|
SiteUpload
|
||||||
|
@ -309,7 +308,6 @@ module Mastodon::CLI
|
||||||
[:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
|
[:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
|
||||||
[:preview_cards, PreviewCard.sum(:image_file_size), nil],
|
[:preview_cards, PreviewCard.sum(:image_file_size), nil],
|
||||||
[:backups, Backup.sum(:dump_file_size), nil],
|
[:backups, Backup.sum(:dump_file_size), nil],
|
||||||
[:imports, Import.sum(:data_file_size), nil],
|
|
||||||
[:settings, SiteUpload.sum(:file_file_size), nil],
|
[:settings, SiteUpload.sum(:file_file_size), nil],
|
||||||
].map { |label, total, local| [label.to_s.titleize, number_to_human_size(total), local.present? ? number_to_human_size(local) : nil] }
|
].map { |label, total, local| [label.to_s.titleize, number_to_human_size(total), local.present? ? number_to_human_size(local) : nil] }
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
Fabricator(:import) do
|
|
||||||
account
|
|
||||||
type :following
|
|
||||||
data { attachment_fixture('imports.txt') }
|
|
||||||
end
|
|
|
@ -1,10 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Import do
|
|
||||||
describe 'Validations' do
|
|
||||||
it { is_expected.to validate_presence_of(:type) }
|
|
||||||
it { is_expected.to validate_presence_of(:data) }
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,242 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe ImportService, :inline_jobs do
|
|
||||||
include RoutingHelper
|
|
||||||
|
|
||||||
let!(:account) { Fabricate(:account, locked: false) }
|
|
||||||
let!(:bob) { Fabricate(:account, username: 'bob', locked: false) }
|
|
||||||
let!(:eve) { Fabricate(:account, username: 'eve', domain: 'example.com', locked: false, protocol: :activitypub, inbox_url: 'https://example.com/inbox') }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_request(:post, 'https://example.com/inbox').to_return(status: 200)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when importing old-style list of muted users' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
|
||||||
|
|
||||||
describe 'when no accounts are muted' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, including notifications' do
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are muted and overwrite is not set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, including notifications' do
|
|
||||||
account.mute!(bob, notifications: false)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are muted and overwrite is set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, including notifications' do
|
|
||||||
account.mute!(bob, notifications: false)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when importing new-style list of muted users' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let(:csv) { attachment_fixture('new-mute-imports.txt') }
|
|
||||||
|
|
||||||
describe 'when no accounts are muted' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, respecting notifications' do
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are muted and overwrite is not set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, respecting notifications' do
|
|
||||||
account.mute!(bob, notifications: true)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are muted and overwrite is set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'muting', data: csv, overwrite: true) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, respecting notifications' do
|
|
||||||
account.mute!(bob, notifications: true)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.muting.count).to eq 2
|
|
||||||
expect(Mute.find_by(account: account, target_account: bob).hide_notifications).to be true
|
|
||||||
expect(Mute.find_by(account: account, target_account: eve).hide_notifications).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when importing old-style list of followed users' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let(:csv) { attachment_fixture('mute-imports.txt') }
|
|
||||||
|
|
||||||
describe 'when no accounts are followed' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
|
||||||
|
|
||||||
it 'follows the listed accounts, including boosts' do
|
|
||||||
subject.call(import)
|
|
||||||
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are already followed and overwrite is not set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
|
||||||
|
|
||||||
it 'follows the listed accounts, including notifications' do
|
|
||||||
account.follow!(bob, reblogs: false)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are already followed and overwrite is set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, including notifications' do
|
|
||||||
account.follow!(bob, reblogs: false)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when importing new-style list of followed users' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let(:csv) { attachment_fixture('new-following-imports.txt') }
|
|
||||||
|
|
||||||
describe 'when no accounts are followed' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
|
||||||
|
|
||||||
it 'follows the listed accounts, respecting boosts' do
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are already followed and overwrite is not set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, respecting notifications' do
|
|
||||||
account.follow!(bob, reblogs: true)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when some accounts are already followed and overwrite is set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv, overwrite: true) }
|
|
||||||
|
|
||||||
it 'mutes the listed accounts, respecting notifications' do
|
|
||||||
account.follow!(bob, reblogs: true)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.following.count).to eq 1
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
expect(Follow.find_by(account: account, target_account: bob).show_reblogs).to be true
|
|
||||||
expect(FollowRequest.find_by(account: account, target_account: eve).show_reblogs).to be false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Based on the bug report 20571 where UTF-8 encoded domains were rejecting import of their users
|
|
||||||
#
|
|
||||||
# https://github.com/mastodon/mastodon/issues/20571
|
|
||||||
context 'with a utf-8 encoded domains' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let!(:nare) { Fabricate(:account, username: 'nare', domain: 'թութ.հայ', locked: false, protocol: :activitypub, inbox_url: 'https://թութ.հայ/inbox') }
|
|
||||||
let(:csv) { attachment_fixture('utf8-followers.txt') }
|
|
||||||
let(:import) { Import.create(account: account, type: 'following', data: csv) }
|
|
||||||
|
|
||||||
# Make sure to not actually go to the remote server
|
|
||||||
before do
|
|
||||||
stub_request(:post, nare.inbox_url).to_return(status: 200)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'follows the listed account' do
|
|
||||||
expect(account.follow_requests.count).to eq 0
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.follow_requests.count).to eq 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when importing bookmarks' do
|
|
||||||
subject { described_class.new }
|
|
||||||
|
|
||||||
let(:csv) { attachment_fixture('bookmark-imports.txt') }
|
|
||||||
let(:local_account) { Fabricate(:account, username: 'foo', domain: nil) }
|
|
||||||
let!(:remote_status) { Fabricate(:status, uri: 'https://example.com/statuses/1312') }
|
|
||||||
let!(:direct_status) { Fabricate(:status, uri: 'https://example.com/statuses/direct', visibility: :direct) }
|
|
||||||
|
|
||||||
around do |example|
|
|
||||||
local_before = Rails.configuration.x.local_domain
|
|
||||||
web_before = Rails.configuration.x.web_domain
|
|
||||||
Rails.configuration.x.local_domain = 'local.com'
|
|
||||||
Rails.configuration.x.web_domain = 'local.com'
|
|
||||||
example.run
|
|
||||||
Rails.configuration.x.web_domain = web_before
|
|
||||||
Rails.configuration.x.local_domain = local_before
|
|
||||||
end
|
|
||||||
|
|
||||||
before do
|
|
||||||
service = instance_double(ActivityPub::FetchRemoteStatusService)
|
|
||||||
allow(ActivityPub::FetchRemoteStatusService).to receive(:new).and_return(service)
|
|
||||||
allow(service).to receive(:call).with('https://unknown-remote.com/users/bar/statuses/1') do
|
|
||||||
Fabricate(:status, uri: 'https://unknown-remote.com/users/bar/statuses/1')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe 'when no bookmarks are set' do
|
|
||||||
let(:import) { Import.create(account: account, type: 'bookmarks', data: csv) }
|
|
||||||
|
|
||||||
it 'adds the toots the user has access to to bookmarks' do
|
|
||||||
local_status = Fabricate(:status, account: local_account, uri: 'https://local.com/users/foo/statuses/42', id: 42, local: true)
|
|
||||||
subject.call(import)
|
|
||||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(local_status.id)
|
|
||||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to include(remote_status.id)
|
|
||||||
expect(account.bookmarks.map { |bookmark| bookmark.status.id }).to_not include(direct_status.id)
|
|
||||||
expect(account.bookmarks.count).to eq 3
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module SystemHelpers
|
module SystemHelpers
|
||||||
|
FRONTEND_TRANSLATIONS = JSON.parse Rails.root.join('app', 'javascript', 'mastodon', 'locales', 'en.json').read
|
||||||
|
|
||||||
def submit_button
|
def submit_button
|
||||||
I18n.t('generic.save_changes')
|
I18n.t('generic.save_changes')
|
||||||
end
|
end
|
||||||
|
@ -16,4 +18,8 @@ module SystemHelpers
|
||||||
def css_id(record)
|
def css_id(record)
|
||||||
"##{dom_id(record)}"
|
"##{dom_id(record)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def frontend_translations(key)
|
||||||
|
FRONTEND_TRANSLATIONS[key]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,7 +18,7 @@ RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
|
||||||
visit_profile(other_account)
|
visit_profile(other_account)
|
||||||
|
|
||||||
note_text = 'This is a personal note'
|
note_text = 'This is a personal note'
|
||||||
fill_in 'Click to add note', with: note_text
|
fill_in frontend_translations('account_note.placeholder'), with: note_text
|
||||||
|
|
||||||
# This is a bit awkward since there is no button to save the change
|
# This is a bit awkward since there is no button to save the change
|
||||||
# The easiest way is to send ctrl+enter ourselves
|
# The easiest way is to send ctrl+enter ourselves
|
||||||
|
|
|
@ -17,8 +17,9 @@ RSpec.describe 'Log out' do
|
||||||
click_on 'Logout'
|
click_on 'Logout'
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(page).to have_title(I18n.t('auth.login'))
|
expect(page)
|
||||||
expect(page).to have_current_path('/auth/sign_in')
|
.to have_title(I18n.t('auth.login'))
|
||||||
|
.and have_current_path('/auth/sign_in')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -28,6 +29,8 @@ RSpec.describe 'Log out' do
|
||||||
ignore_js_error(/Failed to load resource: the server responded with a status of 422/)
|
ignore_js_error(/Failed to load resource: the server responded with a status of 422/)
|
||||||
|
|
||||||
visit root_path
|
visit root_path
|
||||||
|
expect(page)
|
||||||
|
.to have_css('body', class: 'app-body')
|
||||||
|
|
||||||
within '.navigation-bar' do
|
within '.navigation-bar' do
|
||||||
click_on 'Menu'
|
click_on 'Menu'
|
||||||
|
@ -39,8 +42,9 @@ RSpec.describe 'Log out' do
|
||||||
|
|
||||||
click_on 'Log out'
|
click_on 'Log out'
|
||||||
|
|
||||||
expect(page).to have_title(I18n.t('auth.login'))
|
expect(page)
|
||||||
expect(page).to have_current_path('/auth/sign_in')
|
.to have_title(I18n.t('auth.login'))
|
||||||
|
.and have_current_path('/auth/sign_in')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,20 +17,7 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
|
||||||
status_text = 'This is a new status!'
|
status_text = 'This is a new status!'
|
||||||
|
|
||||||
within('.compose-form') do
|
within('.compose-form') do
|
||||||
fill_in "What's on your mind?", with: status_text
|
fill_in frontend_translations('compose_form.placeholder'), with: status_text
|
||||||
click_on 'Post'
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(page)
|
|
||||||
.to have_css('.status__content__text', text: status_text)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'can be posted again' do
|
|
||||||
visit_homepage
|
|
||||||
status_text = 'This is a second status!'
|
|
||||||
|
|
||||||
within('.compose-form') do
|
|
||||||
fill_in "What's on your mind?", with: status_text
|
|
||||||
click_on 'Post'
|
click_on 'Post'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -23,24 +23,14 @@ RSpec.describe 'Share page', :js, :streaming do
|
||||||
fill_in_form
|
fill_in_form
|
||||||
|
|
||||||
expect(page)
|
expect(page)
|
||||||
.to have_css('.notification-bar-message', text: translations['compose.published.body'])
|
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_in_form
|
def fill_in_form
|
||||||
within('.compose-form') do
|
within('.compose-form') do
|
||||||
fill_in translations['compose_form.placeholder'],
|
fill_in frontend_translations('compose_form.placeholder'),
|
||||||
with: 'This is a new status!'
|
with: 'This is a new status!'
|
||||||
click_on translations['compose_form.publish']
|
click_on frontend_translations('compose_form.publish')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def translations
|
|
||||||
# TODO: Extract to system spec helper for re-use?
|
|
||||||
JSON.parse(
|
|
||||||
Rails
|
|
||||||
.root
|
|
||||||
.join('app', 'javascript', 'mastodon', 'locales', 'en.json')
|
|
||||||
.read
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe ImportWorker do
|
|
||||||
let(:worker) { described_class.new }
|
|
||||||
let(:service) { instance_double(ImportService, call: true) }
|
|
||||||
|
|
||||||
describe '#perform' do
|
|
||||||
before do
|
|
||||||
allow(ImportService).to receive(:new).and_return(service)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:import) { Fabricate(:import) }
|
|
||||||
|
|
||||||
it 'sends the import to the service' do
|
|
||||||
worker.perform(import.id)
|
|
||||||
|
|
||||||
expect(service).to have_received(:call).with(import)
|
|
||||||
expect { import.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
Reference in New Issue
Block a user