diff --git a/Gemfile.lock b/Gemfile.lock
index 865686a5bd..86cfaa3132 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -395,7 +395,7 @@ GEM
rexml
link_header (0.0.8)
lint_roller (1.1.0)
- linzer (0.6.4)
+ linzer (0.6.5)
openssl (~> 3.0, >= 3.0.0)
rack (>= 2.2, < 4.0)
starry (~> 0.2)
diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx
index 55f1e6fb91..f6241504f6 100644
--- a/app/javascript/mastodon/components/account.tsx
+++ b/app/javascript/mastodon/components/account.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -12,6 +12,7 @@ import {
muteAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Avatar } from 'mastodon/components/avatar';
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 { Skeleton } from 'mastodon/components/skeleton';
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';
const messages = defineMessages({
@@ -46,6 +47,14 @@ const messages = defineMessages({
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
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<{
@@ -60,6 +69,7 @@ export const Account: React.FC<{
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
+ const accountUrl = account?.url;
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
@@ -77,13 +87,62 @@ export const Account: React.FC<{
}
}, [dispatch, id, account, relationship]);
- const handleMuteNotifications = useCallback(() => {
- dispatch(muteAccount(id, true));
- }, [dispatch, id]);
+ const menu = useMemo(() => {
+ let arr: MenuItem[] = [];
- const handleUnmuteNotifications = useCallback(() => {
- dispatch(muteAccount(id, false));
- }, [dispatch, id]);
+ if (defaultAction === 'mute') {
+ const handleMuteNotifications = () => {
+ 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) {
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) {
- const { requested, blocking, muting } = relationship;
-
- if (requested) {
- buttons = ;
- } else if (blocking) {
- buttons = (
-
- );
- } else if (muting) {
- const menu = [
- {
- text: intl.formatMessage(
- relationship.muting_notifications
- ? messages.unmute_notifications
- : messages.mute_notifications,
- ),
- action: relationship.muting_notifications
- ? handleUnmuteNotifications
- : handleMuteNotifications,
- },
- ];
-
- buttons = (
- <>
-
-
-
- >
- );
- } else if (defaultAction === 'mute') {
- buttons = (
-
- );
- } else if (defaultAction === 'block') {
- buttons = (
-
- );
- } else {
- buttons = ;
- }
- } else {
- buttons = ;
+ if (menu.length > 0) {
+ dropdown = (
+
+ );
}
- let muteTimeRemaining;
+ if (defaultAction === 'block') {
+ button = (
+
+ );
+ } else if (defaultAction === 'mute') {
+ button = (
+
+ );
+ } else {
+ button = ;
+ }
+
+ let muteTimeRemaining: React.ReactNode;
if (account?.mute_expires_at) {
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);
@@ -211,7 +244,12 @@ export const Account: React.FC<{
- {!minimal &&
{buttons}
}
+ {!minimal && (
+
+ {dropdown}
+ {button}
+
+ )}
{account &&
diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx
index 1fe41e1e8b..346c95183f 100644
--- a/app/javascript/mastodon/components/hashtag.tsx
+++ b/app/javascript/mastodon/components/hashtag.tsx
@@ -102,7 +102,7 @@ export interface HashtagProps {
description?: React.ReactNode;
history?: number[];
name: string;
- people: number;
+ people?: number;
to: string;
uses?: number;
withGraph?: boolean;
diff --git a/app/javascript/mastodon/components/navigation_portal.tsx b/app/javascript/mastodon/components/navigation_portal.tsx
index 08f91ce18a..d3ac8baa6e 100644
--- a/app/javascript/mastodon/components/navigation_portal.tsx
+++ b/app/javascript/mastodon/components/navigation_portal.tsx
@@ -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 { showTrends } from 'mastodon/initial_state';
-const DefaultNavigation: React.FC = () => (showTrends ? : null);
-
export const NavigationPortal: React.FC = () => (
-
-
-
-
-
-
-
-
-
-
-
+ {showTrends && }
);
diff --git a/app/javascript/mastodon/components/remote_hint.tsx b/app/javascript/mastodon/components/remote_hint.tsx
new file mode 100644
index 0000000000..772aa805db
--- /dev/null
+++ b/app/javascript/mastodon/components/remote_hint.tsx
@@ -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 = ({ 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 (
+
+ }
+ label={
+ {domain} }}
+ />
+ }
+ />
+ );
+};
diff --git a/app/javascript/mastodon/features/account/components/featured_tags.jsx b/app/javascript/mastodon/features/account/components/featured_tags.jsx
deleted file mode 100644
index 56a9efac02..0000000000
--- a/app/javascript/mastodon/features/account/components/featured_tags.jsx
+++ /dev/null
@@ -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 (
-
-
}} />
-
- {featuredTags.take(3).map(featuredTag => (
- 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)}
- />
- ))}
-
- );
- }
-
-}
-
-export default injectIntl(FeaturedTags);
diff --git a/app/javascript/mastodon/features/account/containers/featured_tags_container.js b/app/javascript/mastodon/features/account/containers/featured_tags_container.js
deleted file mode 100644
index 726c805f78..0000000000
--- a/app/javascript/mastodon/features/account/containers/featured_tags_container.js
+++ /dev/null
@@ -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);
diff --git a/app/javascript/mastodon/features/account/navigation.jsx b/app/javascript/mastodon/features/account/navigation.jsx
deleted file mode 100644
index aa78135de2..0000000000
--- a/app/javascript/mastodon/features/account/navigation.jsx
+++ /dev/null
@@ -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 (
-
- );
- }
-
-}
-
-export default connect(mapStateToProps)(AccountNavigation);
diff --git a/app/javascript/mastodon/features/account_featured/components/empty_message.tsx b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
new file mode 100644
index 0000000000..9dd8ffdfe0
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/components/empty_message.tsx
@@ -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 = ({
+ accountId,
+ suspended,
+ hidden,
+ blockedBy,
+}) => {
+ if (!accountId) {
+ return null;
+ }
+
+ let message: React.ReactNode = null;
+
+ if (suspended) {
+ message = (
+
+ );
+ } else if (hidden) {
+ message = ;
+ } else if (blockedBy) {
+ message = (
+
+ );
+ } else {
+ message = (
+
+ );
+ }
+
+ return {message}
;
+};
diff --git a/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx
new file mode 100644
index 0000000000..7b476ba01d
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/components/featured_tag.tsx
@@ -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 = ({ tag, account }) => {
+ const intl = useIntl();
+ const name = tag.get('name') ?? '';
+ const count = Number.parseInt(tag.get('statuses_count') ?? '');
+ return (
+ 0
+ ? intl.formatMessage(messages.lastStatusAt, {
+ date: intl.formatDate(tag.get('last_status_at') ?? '', {
+ month: 'short',
+ day: '2-digit',
+ }),
+ })
+ : intl.formatMessage(messages.empty)
+ }
+ />
+ );
+};
diff --git a/app/javascript/mastodon/features/account_featured/index.tsx b/app/javascript/mastodon/features/account_featured/index.tsx
new file mode 100644
index 0000000000..70e411f61a
--- /dev/null
+++ b/app/javascript/mastodon/features/account_featured/index.tsx
@@ -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();
+
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ if (accountId) {
+ void dispatch(expandAccountFeaturedTimeline(accountId));
+ dispatch(fetchFeaturedTags(accountId));
+ }
+ }, [accountId, dispatch]);
+
+ const isLoading = useAppSelector(
+ (state) =>
+ !accountId ||
+ !!(state.timelines as ImmutableMap).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,
+ );
+ const featuredStatusIds = useAppSelector(
+ (state) =>
+ (state.timelines as ImmutableMap).getIn(
+ [`account:${accountId}:pinned`, 'items'],
+ ImmutableList(),
+ ) as ImmutableList,
+ );
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (featuredStatusIds.isEmpty() && featuredTags.isEmpty()) {
+ return (
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ {accountId && (
+
+ )}
+ {!featuredTags.isEmpty() && (
+ <>
+
+
+
+ {featuredTags.map((tag) => (
+
+ ))}
+ >
+ )}
+ {!featuredStatusIds.isEmpty() && (
+ <>
+
+
+
+ {featuredStatusIds.map((statusId) => (
+
+ ))}
+ >
+ )}
+
+
+
+ );
+};
+
+const AccountFeaturedWrapper = ({
+ children,
+ accountId,
+}: React.PropsWithChildren<{ accountId?: string }>) => {
+ return (
+
+
+
+ {accountId &&
}
+ {children}
+
+
+ );
+};
+
+// eslint-disable-next-line import/no-default-export
+export default AccountFeatured;
diff --git a/app/javascript/mastodon/features/account_gallery/index.tsx b/app/javascript/mastodon/features/account_gallery/index.tsx
index 60afdadc81..0027329c93 100644
--- a/app/javascript/mastodon/features/account_gallery/index.tsx
+++ b/app/javascript/mastodon/features/account_gallery/index.tsx
@@ -2,25 +2,22 @@ import { useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
-import { useParams } from 'react-router-dom';
-
import { createSelector } from '@reduxjs/toolkit';
import type { Map as ImmutableMap } from 'immutable';
import { List as ImmutableList } from 'immutable';
-import { lookupAccount, fetchAccount } from 'mastodon/actions/accounts';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
+import { RemoteHint } from 'mastodon/components/remote_hint';
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 { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
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 { normalizeForLookup } from 'mastodon/reducers/accounts_map';
-import { getAccountHidden } from 'mastodon/selectors/accounts';
import type { RootState } 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 (
-
- }
- label={
- {domain} }}
- />
- }
- />
- );
-};
-
export const AccountGallery: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
- const { acct, id } = useParams();
const dispatch = useAppDispatch();
- const accountId = useAppSelector(
- (state) =>
- id ??
- (state.accounts_map.get(normalizeForLookup(acct)) as string | undefined),
- );
+ const accountId = useAccountId();
const attachments = useAppSelector((state) =>
accountId
? getAccountGallery(state, accountId)
@@ -123,33 +78,15 @@ export const AccountGallery: React.FC<{
const account = useAppSelector((state) =>
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 remote = account?.acct !== account?.username;
- const hidden = useAppSelector((state) =>
- accountId ? getAccountHidden(state, accountId) : false,
- );
+
+ const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
+
const maxId = attachments.last()?.getIn(['status', 'id']) as
| string
| undefined;
useEffect(() => {
- if (!accountId) {
- dispatch(lookupAccount(acct));
- }
- }, [dispatch, accountId, acct]);
-
- useEffect(() => {
- if (accountId && !isAccount) {
- dispatch(fetchAccount(accountId));
- }
-
if (accountId && isAccount) {
void dispatch(expandAccountMediaTimeline(accountId));
}
@@ -233,7 +170,7 @@ export const AccountGallery: React.FC<{
defaultMessage='Profile unavailable'
/>
);
- } else if (remote && attachments.isEmpty()) {
+ } else if (attachments.isEmpty()) {
emptyMessage = ;
} else {
emptyMessage = (
@@ -259,7 +196,7 @@ export const AccountGallery: React.FC<{
)
}
alwaysPrepend
- append={remote && accountId && }
+ append={accountId && }
scrollKey='account_gallery'
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
index ca12834528..c8fb3d2ae7 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -956,6 +956,9 @@ export const AccountHeader: React.FC<{
{!(hideTabs || hidden) && (
+
+
+
diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx
index 886191e668..a5223275b3 100644
--- a/app/javascript/mastodon/features/account_timeline/index.jsx
+++ b/app/javascript/mastodon/features/account_timeline/index.jsx
@@ -7,12 +7,10 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import { TimelineHint } from 'mastodon/components/timeline_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
-import { useAppSelector } from 'mastodon/store';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { fetchFeaturedTags } from '../../actions/featured_tags';
@@ -21,6 +19,7 @@ import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
+import { RemoteHint } from 'mastodon/components/remote_hint';
import { AccountHeader } from './components/account_header';
import { LimitedAccountHint } from './components/limited_account_hint';
@@ -47,11 +46,8 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
return {
accountId,
- remote: !!(state.getIn(['accounts', accountId, 'acct']) !== state.getIn(['accounts', accountId, 'username'])),
- remoteUrl: state.getIn(['accounts', accountId, 'url']),
isAccount: !!state.getIn(['accounts', accountId]),
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']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
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 (
-
}
- label={
{domain} }} />}
- />
- );
-};
-
-RemoteHint.propTypes = {
- url: PropTypes.string.isRequired,
- accountId: PropTypes.string.isRequired,
-};
-
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
@@ -89,7 +67,6 @@ class AccountTimeline extends ImmutablePureComponent {
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
- featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
@@ -97,8 +74,6 @@ class AccountTimeline extends ImmutablePureComponent {
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
- remote: PropTypes.bool,
- remoteUrl: PropTypes.string,
multiColumn: PropTypes.bool,
};
@@ -161,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
};
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()) {
return (
@@ -191,8 +166,6 @@ class AccountTimeline extends ImmutablePureComponent {
emptyMessage = ;
}
- const remoteMessage = remote ? : null;
-
return (
@@ -200,10 +173,9 @@ class AccountTimeline extends ImmutablePureComponent {
}
alwaysPrepend
- append={remoteMessage}
+ append={}
scrollKey='account_timeline'
statusIds={forceEmptyState ? emptyList : statusIds}
- featuredStatusIds={featuredStatusIds}
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={this.handleLoadMore}
diff --git a/app/javascript/mastodon/features/notifications/components/notification_request.jsx b/app/javascript/mastodon/features/notifications/components/notification_request.jsx
index 9c9365d088..381bb1153f 100644
--- a/app/javascript/mastodon/features/notifications/components/notification_request.jsx
+++ b/app/javascript/mastodon/features/notifications/components/notification_request.jsx
@@ -105,11 +105,10 @@ export const NotificationRequest = ({ id, accountId, notificationsCount, checked
-
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index a1cb8212d2..bb9720c17f 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -73,6 +73,7 @@ import {
About,
PrivacyPolicy,
TermsOfService,
+ AccountFeatured,
} from './util/async-components';
import { ColumnsContextProvider } from './util/columns_context';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
@@ -236,6 +237,7 @@ class SwitchingColumnsArea extends PureComponent {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 8c3b342778..ec493ae283 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -66,6 +66,10 @@ export function AccountGallery () {
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
}
+export function AccountFeatured() {
+ return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
+}
+
export function Followers () {
return import(/* webpackChunkName: "features/followers" */'../../followers');
}
diff --git a/app/javascript/mastodon/hooks/useAccountId.ts b/app/javascript/mastodon/hooks/useAccountId.ts
new file mode 100644
index 0000000000..1cc819ca59
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useAccountId.ts
@@ -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();
+ 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;
+}
diff --git a/app/javascript/mastodon/hooks/useAccountVisibility.ts b/app/javascript/mastodon/hooks/useAccountVisibility.ts
new file mode 100644
index 0000000000..55651af5a0
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useAccountVisibility.ts
@@ -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,
+ };
+}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index ebd5412cf2..0a0f043b4d 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -27,9 +27,11 @@
"account.edit_profile": "Edit profile",
"account.enable_notifications": "Notify me when @{name} posts",
"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_never": "No posts",
- "account.featured_tags.title": "{name}'s featured hashtags",
"account.follow": "Follow",
"account.follow_back": "Follow back",
"account.followers": "Followers",
@@ -294,6 +296,7 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"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_suspended": "Account suspended",
"empty_column.account_timeline": "No posts here!",
diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json
index f4758e5afb..27f0bf4e9a 100644
--- a/app/javascript/mastodon/locales/fa.json
+++ b/app/javascript/mastodon/locales/fa.json
@@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} فرسته} other {{counter} فرسته}}",
"account.unblock": "رفع مسدودیت @{name}",
"account.unblock_domain": "رفع مسدودیت دامنهٔ {domain}",
+ "account.unblock_domain_short": "آنبلاک",
"account.unblock_short": "رفع مسدودیت",
"account.unendorse": "معرّفی نکردن در نمایه",
"account.unfollow": "پینگرفتن",
diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json
index 3ac1946503..5279159d79 100644
--- a/app/javascript/mastodon/locales/pt-BR.json
+++ b/app/javascript/mastodon/locales/pt-BR.json
@@ -65,6 +65,7 @@
"account.statuses_counter": "{count, plural, one {{counter} publicação} other {{counter} publicações}}",
"account.unblock": "Desbloquear @{name}",
"account.unblock_domain": "Desbloquear domínio {domain}",
+ "account.unblock_domain_short": "Desbloquear",
"account.unblock_short": "Desbloquear",
"account.unendorse": "Remover",
"account.unfollow": "Deixar de seguir",
diff --git a/app/lib/admin/metrics/dimension/space_usage_dimension.rb b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
index f1b6dba040..0d3fd8db33 100644
--- a/app/lib/admin/metrics/dimension/space_usage_dimension.rb
+++ b/app/lib/admin/metrics/dimension/space_usage_dimension.rb
@@ -45,7 +45,6 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
PreviewCard.sum(:image_file_size),
Account.sum(Arel.sql('COALESCE(avatar_file_size, 0) + COALESCE(header_file_size, 0)')),
Backup.sum(:dump_file_size),
- Import.sum(:data_file_size),
SiteUpload.sum(:file_file_size),
].sum
diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb
index 34f6199ec0..676e9e62a0 100644
--- a/app/lib/status_cache_hydrator.rb
+++ b/app/lib/status_cache_hydrator.rb
@@ -26,12 +26,7 @@ class StatusCacheHydrator
def hydrate_non_reblog_payload(empty_payload, account_id)
empty_payload.tap do |payload|
- 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)
+ fill_status_payload(payload, @status, account_id)
if payload[:poll]
payload[:poll][:voted] = @status.account_id == account_id
@@ -45,18 +40,12 @@ class StatusCacheHydrator
payload[:muted] = false
payload[:bookmarked] = false
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
# 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][:favourited] = Favourite.exists?(account_id: account_id, status_id: @status.reblog_of_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]
+ fill_status_payload(payload[:reblog], @status.reblog, account_id)
if payload[:reblog][:poll]
if @status.reblog.account_id == account_id
@@ -69,11 +58,21 @@ class StatusCacheHydrator
end
end
+ payload[:filtered] = payload[:reblog][:filtered]
payload[:favourited] = payload[:reblog][:favourited]
payload[:reblogged] = payload[:reblog][:reblogged]
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)
CustomFilter
.apply_cached_filters(CustomFilter.cached_filters_for(account_id), status)
diff --git a/app/models/import.rb b/app/models/import.rb
deleted file mode 100644
index 4bdb392014..0000000000
--- a/app/models/import.rb
+++ /dev/null
@@ -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
diff --git a/app/services/import_service.rb b/app/services/import_service.rb
deleted file mode 100644
index a695df2fc9..0000000000
--- a/app/services/import_service.rb
+++ /dev/null
@@ -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
diff --git a/app/workers/import/relationship_worker.rb b/app/workers/import/relationship_worker.rb
deleted file mode 100644
index 2298b095a7..0000000000
--- a/app/workers/import/relationship_worker.rb
+++ /dev/null
@@ -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
diff --git a/app/workers/import_worker.rb b/app/workers/import_worker.rb
deleted file mode 100644
index b6afb972a9..0000000000
--- a/app/workers/import_worker.rb
+++ /dev/null
@@ -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
diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb
index ed16d50a76..6d908fa477 100644
--- a/config/initializers/paperclip.rb
+++ b/config/initializers/paperclip.rb
@@ -169,7 +169,7 @@ else
end
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
# In some places in the code, we rescue this exception, but we don't always
diff --git a/config/locales/fa.yml b/config/locales/fa.yml
index b5dce6dabf..f1c74829c9 100644
--- a/config/locales/fa.yml
+++ b/config/locales/fa.yml
@@ -479,6 +479,22 @@ fa:
new:
title: درونریزی انسدادهای دامنه
no_file: هیچ پروندهای گزیده نشده
+ fasp:
+ debug:
+ callbacks:
+ delete: حذف
+ providers:
+ active: فعال
+ delete: حذف
+ finish_registration: تکمیل ثبتنام
+ name: نام
+ providers: ارائه دهندگان
+ registrations:
+ confirm: تایید
+ reject: رد کردن
+ save: ذخیره
+ sign_in: ورود
+ status: وضعیت
follow_recommendations:
description_html: "پیشنهادات پیگیری به کاربران جدید کک میکند تا سریعتر محتوای جالب را پیدا کنند. زمانی که کاربری هنوز به اندازه کافی با دیگران تعامل نداشته است تا پیشنهادات پیگیری شخصیسازیشده دریافت کند، این حسابها را به جای آن فهرست مشاهده خواهد کرد. این حسابها به صورت روزانه و در ترکیب با بیشتری تعاملات و بالاترین دنبالکنندگان محلی برای یک زبان مشخص بازمحاسبه میشوند."
language: برای زبان
diff --git a/config/locales/lv.yml b/config/locales/lv.yml
index 151cc6cf6c..fad8a2609e 100644
--- a/config/locales/lv.yml
+++ b/config/locales/lv.yml
@@ -259,6 +259,7 @@ lv:
create_user_role_html: "%{name} nomainīja %{target} lomu"
demote_user_html: "%{name} pazemināja lietotāju %{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_domain_allow_html: "%{name} neatļāva federāciju ar 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.
preamble_html: Tu gatavojies apturēt domēna %{domain} 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.
- 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
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
@@ -949,11 +950,13 @@ lv:
message_html: "Tava objektu krātuve ir nepareizi konfigurēta. Tavu lietotāju privātums ir apdraudēts."
tags:
moderation:
+ not_trendable: Nav izplatīts
not_usable: Nav izmantojams
pending_review: Gaida pārskatīšanu
review_requested: Pieprasīta pārskatīšana
reviewed: Pārskatīts
title: Stāvoklis
+ trendable: Izplatīts
unreviewed: Nepārskatīts
usable: Izmantojams
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_rejected: Apelācija ir noraidīta
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:
submit: Iesniegt apelāciju
approve_appeal: Apstiprināt apelāciju
@@ -1332,9 +1335,9 @@ lv:
sensitive: Konta atzīmēšana kā jūtīgu
silence: Konta ierobežoš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_rejected: Jūsu apelācija ir noraidīta
+ your_appeal_rejected: Tava pārsūdzība tika noraidīta
edit_profile:
basic_information: Pamata informācija
hint_html: "Pielāgo, ko cilvēki redz Tavā publiskajā profilā un blakus Taviem ierakstiem. 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
min_age_label: Vecuma slieksnis
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_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:
@@ -1978,12 +1981,13 @@ lv:
title: "%{domain} pakalpojuma paziņojums"
appeal_approved:
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ī.
- subject: Jūsu %{date} apelācija ir apstiprināta
+ 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: Tava %{date} iesniegtā pārsūdzība tika apstiprināta
title: Apelācija apstiprināta
appeal_rejected:
- explanation: Apelācija par brīdinājumu jūsu kontam %{strike_date}, ko iesniedzāt %{appeal_date}, ir noraidīta.
- subject: Jūsu %{date} apelācija ir noraidīta
+ explanation: Pārsūdzība par brīdinājumu Tavam kontam %{strike_date}, ko iesniedzi %{appeal_date}, tika 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
backup_ready:
explanation: Tu pieprasīji pilnu sava Mastodon konta rezerves kopiju.
@@ -1993,6 +1997,9 @@ lv:
failed_2fa:
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.
+ 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:
change_password: mainīt paroli
details: 'Šeit ir pieteikšanās izvērsums:'
diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml
index 6a8fdda024..a2e142ab55 100644
--- a/config/locales/pt-BR.yml
+++ b/config/locales/pt-BR.yml
@@ -320,6 +320,7 @@ pt-BR:
title: Novo anúncio
preview:
explanation_html: 'Esse e-mail será enviado a %{display_count} usuários. O texto a seguir será incluído ao e-mail:'
+ title: Visualizar anúncio
publish: Publicar
published_msg: Anúncio publicado!
scheduled_for: Agendado para %{time}
@@ -484,19 +485,30 @@ pt-BR:
created_at: Criado em
delete: Apagar
ip: Endereço de IP
+ request_body: Corpo da solicitação
+ title: Depurar Callbacks
providers:
+ active: Ativo
base_url: URL Base
+ callback: Callback
delete: Apagar
+ edit: Editar provedor
finish_registration: Finalizar o cadastro
name: Nome
+ providers: Provedores
public_key_fingerprint: Impressão digital de chave pública
registration_requested: Cadastro solicitado
registrations:
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
+ title: Confirmar o registro FASP
save: Salvar
+ select_capabilities: Selecionar recursos
sign_in: Entrar
status: Estado
+ title: Provedores de serviços auxiliares do Fediverso
+ title: FASP
follow_recommendations:
description_html: "A recomendação de contas ajuda os novos usuários a encontrar rapidamente conteúdo interessante. 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
@@ -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. Mantenha os códigos de recuperação em um local seguro. Por exemplo, você pode imprimi-los e guardá-los junto a outros documentos importantes.
webauthn: Chaves de segurança
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:
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.
diff --git a/config/locales/simple_form.fa.yml b/config/locales/simple_form.fa.yml
index 520b684847..9f46cdec7d 100644
--- a/config/locales/simple_form.fa.yml
+++ b/config/locales/simple_form.fa.yml
@@ -349,6 +349,9 @@ fa:
jurisdiction: صلاحیت قانونی
min_age: کمینهٔ زمان
user:
+ date_of_birth_1i: روز
+ date_of_birth_2i: ماه
+ date_of_birth_3i: سال
role: نقش
time_zone: منطقهٔ زمانی
user_role:
diff --git a/config/routes.rb b/config/routes.rb
index 5b130c517b..2fff44851e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -129,6 +129,7 @@ Rails.application.routes.draw do
constraints(username: %r{[^@/.]+}) do
with_options to: 'accounts#show' do
get '/@:username', as: :short_account
+ get '/@:username/featured'
get '/@:username/with_replies', as: :short_account_with_replies
get '/@:username/media', as: :short_account_media
get '/@:username/tagged/:tag', as: :short_account_tag
diff --git a/db/migrate/20250410144908_drop_imports.rb b/db/migrate/20250410144908_drop_imports.rb
new file mode 100644
index 0000000000..7be9daf750
--- /dev/null
+++ b/db/migrate/20250410144908_drop_imports.rb
@@ -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
diff --git a/db/schema.rb b/db/schema.rb
index 26db259464..b09360ff43 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# 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
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"
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|
t.bigint "user_id", 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 "generated_annual_reports", "accounts"
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 "list_accounts", "accounts", on_delete: :cascade
add_foreign_key "list_accounts", "follow_requests", on_delete: :cascade
diff --git a/lib/mastodon/cli/media.rb b/lib/mastodon/cli/media.rb
index 84ec13eaab..1059eb6066 100644
--- a/lib/mastodon/cli/media.rb
+++ b/lib/mastodon/cli/media.rb
@@ -293,7 +293,6 @@ module Mastodon::CLI
Account
Backup
CustomEmoji
- Import
MediaAttachment
PreviewCard
SiteUpload
@@ -309,7 +308,6 @@ module Mastodon::CLI
[:headers, Account.sum(:header_file_size), Account.local.sum(:header_file_size)],
[:preview_cards, PreviewCard.sum(:image_file_size), nil],
[:backups, Backup.sum(:dump_file_size), nil],
- [:imports, Import.sum(:data_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] }
end
diff --git a/spec/fabricators/import_fabricator.rb b/spec/fabricators/import_fabricator.rb
deleted file mode 100644
index 4951bb9a4d..0000000000
--- a/spec/fabricators/import_fabricator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-Fabricator(:import) do
- account
- type :following
- data { attachment_fixture('imports.txt') }
-end
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
deleted file mode 100644
index 587e0a9d26..0000000000
--- a/spec/models/import_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/services/import_service_spec.rb b/spec/services/import_service_spec.rb
deleted file mode 100644
index 2e1358c635..0000000000
--- a/spec/services/import_service_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/support/system_helpers.rb b/spec/support/system_helpers.rb
index ffbba177b3..44bbc64a59 100644
--- a/spec/support/system_helpers.rb
+++ b/spec/support/system_helpers.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
module SystemHelpers
+ FRONTEND_TRANSLATIONS = JSON.parse Rails.root.join('app', 'javascript', 'mastodon', 'locales', 'en.json').read
+
def submit_button
I18n.t('generic.save_changes')
end
@@ -16,4 +18,8 @@ module SystemHelpers
def css_id(record)
"##{dom_id(record)}"
end
+
+ def frontend_translations(key)
+ FRONTEND_TRANSLATIONS[key]
+ end
end
diff --git a/spec/system/account_notes_spec.rb b/spec/system/account_notes_spec.rb
index c4054f204e..1d125e1984 100644
--- a/spec/system/account_notes_spec.rb
+++ b/spec/system/account_notes_spec.rb
@@ -18,7 +18,7 @@ RSpec.describe 'Account notes', :inline_jobs, :js, :streaming do
visit_profile(other_account)
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
# The easiest way is to send ctrl+enter ourselves
diff --git a/spec/system/log_out_spec.rb b/spec/system/log_out_spec.rb
index 2e52254ca0..ebbf5a5772 100644
--- a/spec/system/log_out_spec.rb
+++ b/spec/system/log_out_spec.rb
@@ -17,8 +17,9 @@ RSpec.describe 'Log out' do
click_on 'Logout'
end
- expect(page).to have_title(I18n.t('auth.login'))
- expect(page).to have_current_path('/auth/sign_in')
+ expect(page)
+ .to have_title(I18n.t('auth.login'))
+ .and have_current_path('/auth/sign_in')
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/)
visit root_path
+ expect(page)
+ .to have_css('body', class: 'app-body')
within '.navigation-bar' do
click_on 'Menu'
@@ -39,8 +42,9 @@ RSpec.describe 'Log out' do
click_on 'Log out'
- expect(page).to have_title(I18n.t('auth.login'))
- expect(page).to have_current_path('/auth/sign_in')
+ expect(page)
+ .to have_title(I18n.t('auth.login'))
+ .and have_current_path('/auth/sign_in')
end
end
end
diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb
index 480c77cf87..d14bcc0058 100644
--- a/spec/system/new_statuses_spec.rb
+++ b/spec/system/new_statuses_spec.rb
@@ -17,20 +17,7 @@ RSpec.describe 'NewStatuses', :inline_jobs, :js, :streaming do
status_text = 'This is a new status!'
within('.compose-form') do
- fill_in "What's on your mind?", 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
+ fill_in frontend_translations('compose_form.placeholder'), with: status_text
click_on 'Post'
end
diff --git a/spec/system/share_entrypoint_spec.rb b/spec/system/share_entrypoint_spec.rb
index 7ccfee599a..b55ea31657 100644
--- a/spec/system/share_entrypoint_spec.rb
+++ b/spec/system/share_entrypoint_spec.rb
@@ -23,24 +23,14 @@ RSpec.describe 'Share page', :js, :streaming do
fill_in_form
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
def fill_in_form
within('.compose-form') do
- fill_in translations['compose_form.placeholder'],
+ fill_in frontend_translations('compose_form.placeholder'),
with: 'This is a new status!'
- click_on translations['compose_form.publish']
+ click_on frontend_translations('compose_form.publish')
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
diff --git a/spec/workers/import_worker_spec.rb b/spec/workers/import_worker_spec.rb
deleted file mode 100644
index 1d34aafe86..0000000000
--- a/spec/workers/import_worker_spec.rb
+++ /dev/null
@@ -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