From b06c7b6b5ae28cb81e5ba8ba632ebc629d1fed57 Mon Sep 17 00:00:00 2001
From: Eugen Rochko <eugen@zeonfederated.com>
Date: Wed, 21 Aug 2024 09:08:58 +0200
Subject: [PATCH] Change hints for missing remote content in web UI (#31516)

---
 .../mastodon/components/timeline_hint.tsx     | 27 ++++++++-----------
 .../features/account_timeline/index.jsx       | 21 +++++++++++----
 .../mastodon/features/followers/index.jsx     | 21 +++++++++++----
 .../mastodon/features/following/index.jsx     | 21 +++++++++++----
 .../mastodon/features/status/index.jsx        |  9 ++++++-
 app/javascript/mastodon/locales/en.json       | 14 +++++-----
 .../styles/mastodon/components.scss           | 13 ++++-----
 7 files changed, 82 insertions(+), 44 deletions(-)

diff --git a/app/javascript/mastodon/components/timeline_hint.tsx b/app/javascript/mastodon/components/timeline_hint.tsx
index 6faad2fbb8..9e0d3676ec 100644
--- a/app/javascript/mastodon/components/timeline_hint.tsx
+++ b/app/javascript/mastodon/components/timeline_hint.tsx
@@ -1,28 +1,23 @@
-import { FormattedMessage } from 'react-intl';
-
 import classNames from 'classnames';
 
 interface Props {
-  resource: JSX.Element;
+  message: React.ReactNode;
+  label: React.ReactNode;
   url: string;
   className?: string;
 }
 
-export const TimelineHint: React.FC<Props> = ({ className, resource, url }) => (
+export const TimelineHint: React.FC<Props> = ({
+  className,
+  message,
+  label,
+  url,
+}) => (
   <div className={classNames('timeline-hint', className)}>
-    <strong>
-      <FormattedMessage
-        id='timeline_hint.remote_resource_not_displayed'
-        defaultMessage='{resource} from other servers are not displayed.'
-        values={{ resource }}
-      />
-    </strong>
-    <br />
+    <p>{message}</p>
+
     <a href={url} target='_blank' rel='noopener noreferrer'>
-      <FormattedMessage
-        id='account.browse_more_on_origin_server'
-        defaultMessage='Browse more on the original profile'
-      />
+      {label}
     </a>
   </div>
 );
diff --git a/app/javascript/mastodon/features/account_timeline/index.jsx b/app/javascript/mastodon/features/account_timeline/index.jsx
index dc69f83e77..105c2e4e50 100644
--- a/app/javascript/mastodon/features/account_timeline/index.jsx
+++ b/app/javascript/mastodon/features/account_timeline/index.jsx
@@ -12,6 +12,7 @@ import BundleColumnError from 'mastodon/features/ui/components/bundle_column_err
 import { me } from 'mastodon/initial_state';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
 import { getAccountHidden } from 'mastodon/selectors';
+import { useAppSelector } from 'mastodon/store';
 
 import { lookupAccount, fetchAccount } from '../../actions/accounts';
 import { fetchFeaturedTags } from '../../actions/featured_tags';
@@ -59,12 +60,22 @@ const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = fa
   };
 };
 
-const RemoteHint = ({ url }) => (
-  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.statuses' defaultMessage='Older posts' />} />
-);
+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 {
@@ -175,12 +186,12 @@ class AccountTimeline extends ImmutablePureComponent {
     } else if (blockedBy) {
       emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
     } else if (remote && statusIds.isEmpty()) {
-      emptyMessage = <RemoteHint url={remoteUrl} />;
+      emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
     } else {
       emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
     }
 
-    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+    const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
 
     return (
       <Column>
diff --git a/app/javascript/mastodon/features/followers/index.jsx b/app/javascript/mastodon/features/followers/index.jsx
index 4885f9ca99..92fce79c35 100644
--- a/app/javascript/mastodon/features/followers/index.jsx
+++ b/app/javascript/mastodon/features/followers/index.jsx
@@ -12,6 +12,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
 import { getAccountHidden } from 'mastodon/selectors';
+import { useAppSelector } from 'mastodon/store';
 
 import {
   lookupAccount,
@@ -51,12 +52,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
   };
 };
 
-const RemoteHint = ({ url }) => (
-  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.followers' defaultMessage='Followers' />} />
-);
+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.followers_may_be_missing' defaultMessage='Followers for this profile may be missing.' />}
+      label={<FormattedMessage id='hints.profiles.see_more_followers' defaultMessage='See more followers on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
+    />
+  );
+};
 
 RemoteHint.propTypes = {
   url: PropTypes.string.isRequired,
+  accountId: PropTypes.string.isRequired,
 };
 
 class Followers extends ImmutablePureComponent {
@@ -141,12 +152,12 @@ class Followers extends ImmutablePureComponent {
     } else if (hideCollections && accountIds.isEmpty()) {
       emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
     } else if (remote && accountIds.isEmpty()) {
-      emptyMessage = <RemoteHint url={remoteUrl} />;
+      emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
     } else {
       emptyMessage = <FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />;
     }
 
-    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+    const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
 
     return (
       <Column>
diff --git a/app/javascript/mastodon/features/following/index.jsx b/app/javascript/mastodon/features/following/index.jsx
index fb4a4d5c3a..b53a016ff1 100644
--- a/app/javascript/mastodon/features/following/index.jsx
+++ b/app/javascript/mastodon/features/following/index.jsx
@@ -12,6 +12,7 @@ import { TimelineHint } from 'mastodon/components/timeline_hint';
 import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
 import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
 import { getAccountHidden } from 'mastodon/selectors';
+import { useAppSelector } from 'mastodon/store';
 
 import {
   lookupAccount,
@@ -51,12 +52,22 @@ const mapStateToProps = (state, { params: { acct, id } }) => {
   };
 };
 
-const RemoteHint = ({ url }) => (
-  <TimelineHint url={url} resource={<FormattedMessage id='timeline_hint.resources.follows' defaultMessage='Follows' />} />
-);
+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.follows_may_be_missing' defaultMessage='Follows for this profile may be missing.' />}
+      label={<FormattedMessage id='hints.profiles.see_more_follows' defaultMessage='See more follows on {domain}' values={{ domain: <strong>{domain}</strong> }} />}
+    />
+  );
+};
 
 RemoteHint.propTypes = {
   url: PropTypes.string.isRequired,
+  accountId: PropTypes.string.isRequired,
 };
 
 class Following extends ImmutablePureComponent {
@@ -141,12 +152,12 @@ class Following extends ImmutablePureComponent {
     } else if (hideCollections && accountIds.isEmpty()) {
       emptyMessage = <FormattedMessage id='empty_column.account_hides_collections' defaultMessage='This user has chosen to not make this information available' />;
     } else if (remote && accountIds.isEmpty()) {
-      emptyMessage = <RemoteHint url={remoteUrl} />;
+      emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
     } else {
       emptyMessage = <FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />;
     }
 
-    const remoteMessage = remote ? <RemoteHint url={remoteUrl} /> : null;
+    const remoteMessage = remote ? <RemoteHint accountId={accountId} url={remoteUrl} /> : null;
 
     return (
       <Column>
diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx
index 1d15b13bd4..5f325fe7b8 100644
--- a/app/javascript/mastodon/features/status/index.jsx
+++ b/app/javascript/mastodon/features/status/index.jsx
@@ -629,7 +629,14 @@ class Status extends ImmutablePureComponent {
     const isIndexable = !status.getIn(['account', 'noindex']);
 
     if (!isLocal) {
-      remoteHint = <TimelineHint className={classNames(!!descendants && 'timeline-hint--with-descendants')} url={status.get('url')} resource={<FormattedMessage id='timeline_hint.resources.replies' defaultMessage='Some replies' />} />;
+      remoteHint = (
+        <TimelineHint
+          className={classNames(!!descendants && 'timeline-hint--with-descendants')}
+          url={status.get('url')}
+          message={<FormattedMessage id='hints.threads.replies_may_be_missing' defaultMessage='Replies from other servers may be missing.' />}
+          label={<FormattedMessage id='hints.threads.see_more' defaultMessage='See more replies on {domain}' values={{ domain: <strong>{status.getIn(['account', 'acct']).split('@')[1]}</strong> }} />}
+        />
+      );
     }
 
     const handlers = {
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 6d2b93be57..43ff015791 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -19,7 +19,6 @@
   "account.block_domain": "Block domain {domain}",
   "account.block_short": "Block",
   "account.blocked": "Blocked",
-  "account.browse_more_on_origin_server": "Browse more on the original profile",
   "account.cancel_follow_request": "Cancel follow",
   "account.copy": "Copy link to profile",
   "account.direct": "Privately mention @{name}",
@@ -349,6 +348,14 @@
   "hashtag.follow": "Follow hashtag",
   "hashtag.unfollow": "Unfollow hashtag",
   "hashtags.and_other": "…and {count, plural, other {# more}}",
+  "hints.profiles.followers_may_be_missing": "Followers for this profile may be missing.",
+  "hints.profiles.follows_may_be_missing": "Follows for this profile may be missing.",
+  "hints.profiles.posts_may_be_missing": "Some posts from this profile may be missing.",
+  "hints.profiles.see_more_followers": "See more followers on {domain}",
+  "hints.profiles.see_more_follows": "See more follows on {domain}",
+  "hints.profiles.see_more_posts": "See more posts on {domain}",
+  "hints.threads.replies_may_be_missing": "Replies from other servers may be missing.",
+  "hints.threads.see_more": "See more replies on {domain}",
   "home.column_settings.show_reblogs": "Show boosts",
   "home.column_settings.show_replies": "Show replies",
   "home.hide_announcements": "Hide announcements",
@@ -826,11 +833,6 @@
   "time_remaining.minutes": "{number, plural, one {# minute} other {# minutes}} left",
   "time_remaining.moments": "Moments remaining",
   "time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
-  "timeline_hint.remote_resource_not_displayed": "{resource} from other servers are not displayed.",
-  "timeline_hint.resources.followers": "Followers",
-  "timeline_hint.resources.follows": "Follows",
-  "timeline_hint.resources.replies": "Some replies",
-  "timeline_hint.resources.statuses": "Older posts",
   "trends.counter_by_accounts": "{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {{days} days}}",
   "trends.trending_now": "Trending now",
   "ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 4c47a8db6c..b6e71cde41 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4217,11 +4217,12 @@ a.status-card {
 
 .timeline-hint {
   text-align: center;
-  color: $darker-text-color;
-  padding: 15px;
+  color: $dark-text-color;
+  padding: 16px;
   box-sizing: border-box;
   width: 100%;
-  cursor: default;
+  font-size: 14px;
+  line-height: 21px;
 
   strong {
     font-weight: 500;
@@ -4238,10 +4239,10 @@ a.status-card {
       color: lighten($highlight-text-color, 4%);
     }
   }
-}
 
-.timeline-hint--with-descendants {
-  border-top: 1px solid var(--background-border-color);
+  &--with-descendants {
+    border-top: 1px solid var(--background-border-color);
+  }
 }
 
 .regeneration-indicator {