diff --git a/app/javascript/mastodon/features/explore/components/card.jsx b/app/javascript/mastodon/features/explore/components/card.jsx
new file mode 100644
index 0000000000..316203060a
--- /dev/null
+++ b/app/javascript/mastodon/features/explore/components/card.jsx
@@ -0,0 +1,88 @@
+import PropTypes from 'prop-types';
+import { useCallback } from 'react';
+
+import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
+
+import { Link } from 'react-router-dom';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import CloseIcon from '@/material-icons/400-24px/close.svg?react';
+import { followAccount, unfollowAccount } from 'mastodon/actions/accounts';
+import { dismissSuggestion } from 'mastodon/actions/suggestions';
+import { Avatar } from 'mastodon/components/avatar';
+import { Button } from 'mastodon/components/button';
+import { DisplayName } from 'mastodon/components/display_name';
+import { IconButton } from 'mastodon/components/icon_button';
+import { domain } from 'mastodon/initial_state';
+
+const messages = defineMessages({
+ follow: { id: 'account.follow', defaultMessage: 'Follow' },
+ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+ dismiss: { id: 'follow_suggestions.dismiss', defaultMessage: "Don't show again" },
+});
+
+export const Card = ({ id, source }) => {
+ const intl = useIntl();
+ const account = useSelector(state => state.getIn(['accounts', id]));
+ const relationship = useSelector(state => state.getIn(['relationships', id]));
+ const dispatch = useDispatch();
+ const following = relationship?.get('following') ?? relationship?.get('requested');
+
+ const handleFollow = useCallback(() => {
+ if (following) {
+ dispatch(unfollowAccount(id));
+ } else {
+ dispatch(followAccount(id));
+ }
+ }, [id, following, dispatch]);
+
+ const handleDismiss = useCallback(() => {
+ dispatch(dismissSuggestion(id));
+ }, [id, dispatch]);
+
+ let label;
+
+ switch (source) {
+ case 'friends_of_friends':
+ label = ;
+ break;
+ case 'similar_to_recently_followed':
+ label = ;
+ break;
+ case 'featured':
+ label = ;
+ break;
+ case 'most_followed':
+ label = ;
+ break;
+ case 'most_interactions':
+ label = ;
+ break;
+ }
+
+ return (
+
+ );
+};
+
+Card.propTypes = {
+ id: PropTypes.string.isRequired,
+ source: PropTypes.oneOf(['friends_of_friends', 'similar_to_recently_followed', 'featured', 'most_followed', 'most_interactions']),
+};
diff --git a/app/javascript/mastodon/features/explore/suggestions.jsx b/app/javascript/mastodon/features/explore/suggestions.jsx
index ba33c4d081..101ec0d195 100644
--- a/app/javascript/mastodon/features/explore/suggestions.jsx
+++ b/app/javascript/mastodon/features/explore/suggestions.jsx
@@ -10,9 +10,10 @@ import { connect } from 'react-redux';
import { fetchSuggestions } from 'mastodon/actions/suggestions';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
-import AccountCard from 'mastodon/features/directory/components/account_card';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
+import { Card } from './components/card';
+
const mapStateToProps = state => ({
suggestions: state.getIn(['suggestions', 'items']),
isLoading: state.getIn(['suggestions', 'isLoading']),
@@ -54,7 +55,11 @@ class Suggestions extends PureComponent {
return (
{isLoading ?
: suggestions.map(suggestion => (
-
+
))}
);
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index fd44b3952b..a1b79881c5 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -308,6 +308,8 @@
"follow_requests.unlocked_explanation": "Even though your account is not locked, the {domain} staff thought you might want to review follow requests from these accounts manually.",
"follow_suggestions.curated_suggestion": "Staff pick",
"follow_suggestions.dismiss": "Don't show again",
+ "follow_suggestions.featured_longer": "Hand-picked by the {domain} team",
+ "follow_suggestions.friends_of_friends_longer": "Popular among people you follow",
"follow_suggestions.hints.featured": "This profile has been hand-picked by the {domain} team.",
"follow_suggestions.hints.friends_of_friends": "This profile is popular among the people you follow.",
"follow_suggestions.hints.most_followed": "This profile is one of the most followed on {domain}.",
@@ -315,6 +317,8 @@
"follow_suggestions.hints.similar_to_recently_followed": "This profile is similar to the profiles you have most recently followed.",
"follow_suggestions.personalized_suggestion": "Personalized suggestion",
"follow_suggestions.popular_suggestion": "Popular suggestion",
+ "follow_suggestions.popular_suggestion_longer": "Popular on {domain}",
+ "follow_suggestions.similar_to_recently_followed_longer": "Similar to profiles you recently followed",
"follow_suggestions.view_all": "View all",
"follow_suggestions.who_to_follow": "Who to follow",
"followed_tags": "Followed hashtags",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index b2139169a5..a1864a4562 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -2016,7 +2016,10 @@ a .account__avatar {
display: flex;
align-items: center;
gap: 8px;
+}
+.account__relationship,
+.explore__suggestions__card {
.icon-button {
border: 1px solid var(--background-border-color);
border-radius: 4px;
@@ -2964,6 +2967,75 @@ $ui-header-logo-wordmark-width: 99px;
display: none;
}
+.explore__suggestions__card {
+ padding: 12px 16px;
+ gap: 8px;
+ display: flex;
+ flex-direction: column;
+ border-bottom: 1px solid var(--background-border-color);
+
+ &:last-child {
+ border-bottom: 0;
+ }
+
+ &__source {
+ padding-inline-start: 60px;
+ font-size: 13px;
+ line-height: 16px;
+ color: $dark-text-color;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ &__body {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+
+ &__main {
+ flex: 1 1 auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ min-width: 0;
+
+ &__name-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+
+ &__name {
+ display: block;
+ color: inherit;
+ text-decoration: none;
+ flex: 1 1 auto;
+ min-width: 0;
+ }
+
+ .button {
+ min-width: 80px;
+ }
+
+ .display-name {
+ font-size: 15px;
+ line-height: 20px;
+ color: $secondary-text-color;
+
+ strong {
+ font-weight: 700;
+ }
+
+ &__account {
+ color: $darker-text-color;
+ display: block;
+ }
+ }
+ }
+ }
+ }
+}
+
@media screen and (max-width: $no-gap-breakpoint - 1px) {
.columns-area__panels__pane--compositional {
display: none;
@@ -7293,10 +7365,11 @@ a.status-card {
content: '';
position: absolute;
bottom: -1px;
- left: 0;
- width: 100%;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 40px;
height: 3px;
- border-radius: 4px;
+ border-radius: 4px 4px 0 0;
background: $highlight-text-color;
}
}