diff --git a/app/soapbox/actions/statuses.js b/app/soapbox/actions/statuses.js
index 5b65349b7..440d4150e 100644
--- a/app/soapbox/actions/statuses.js
+++ b/app/soapbox/actions/statuses.js
@@ -3,6 +3,7 @@ import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses } from './importer';
import { openModal } from './modal';
import { isLoggedIn } from 'soapbox/utils/auth';
+import { shouldHaveCard } from 'soapbox/utils/status';
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
@@ -44,8 +45,31 @@ export function createStatus(params, idempotencyKey) {
return api(getState).post('/api/v1/statuses', params, {
headers: { 'Idempotency-Key': idempotencyKey },
}).then(({ data: status }) => {
+ // The backend might still be processing the rich media attachment
+ if (!status.card && shouldHaveCard(status)) {
+ status.expectsCard = true;
+ }
+
dispatch(importFetchedStatus(status, idempotencyKey));
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, idempotencyKey });
+
+ // Poll the backend for the updated card
+ if (status.expectsCard) {
+ const delay = 1000;
+
+ const poll = (retries = 5) => {
+ api(getState).get(`/api/v1/statuses/${status.id}`).then(response => {
+ if (response.data && response.data.card) {
+ dispatch(importFetchedStatus(response.data));
+ } else if (retries > 0 && response.status === 200) {
+ setTimeout(() => poll(retries - 1), delay);
+ }
+ }).catch(console.error);
+ };
+
+ setTimeout(() => poll(), delay);
+ }
+
return status;
}).catch(error => {
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
diff --git a/app/soapbox/components/status.js b/app/soapbox/components/status.js
index de7a330ca..ff7f23f52 100644
--- a/app/soapbox/components/status.js
+++ b/app/soapbox/components/status.js
@@ -19,6 +19,7 @@ import Icon from 'soapbox/components/icon';
import { Link, NavLink } from 'react-router-dom';
import { getDomain } from 'soapbox/utils/accounts';
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
+import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
@@ -465,6 +466,10 @@ class Status extends ImmutablePureComponent {
defaultWidth={this.props.cachedMediaWidth}
/>
);
+ } else if (status.get('expectsCard', false)) {
+ media = (
+
+ );
}
if (otherAccounts && otherAccounts.size > 1) {
diff --git a/app/soapbox/features/placeholder/components/placeholder_card.js b/app/soapbox/features/placeholder/components/placeholder_card.js
new file mode 100644
index 000000000..7f211709d
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_card.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { randomIntFromInterval, generateText } from '../utils';
+
+export default class PlaceholderCard extends React.Component {
+
+ shouldComponentUpdate() {
+ // Re-rendering this will just cause the random lengths to jump around.
+ // There's basically no reason to ever do it.
+ return false;
+ }
+
+ render() {
+ return (
+
+
+
+
{generateText(randomIntFromInterval(5, 25))}
+
+ {generateText(randomIntFromInterval(5, 75))}
+
+
+ {generateText(randomIntFromInterval(5, 15))}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/status/components/card.js b/app/soapbox/features/status/components/card.js
index 4e870640e..2653f9304 100644
--- a/app/soapbox/features/status/components/card.js
+++ b/app/soapbox/features/status/components/card.js
@@ -171,7 +171,7 @@ export default class Card extends React.PureComponent {
const description = (
- {title}
+
{title}
{trim(card.get('description') || '', maxDescription)}
{provider}
diff --git a/app/soapbox/features/ui/components/pending_status.js b/app/soapbox/features/ui/components/pending_status.js
index dd9327979..13daa5928 100644
--- a/app/soapbox/features/ui/components/pending_status.js
+++ b/app/soapbox/features/ui/components/pending_status.js
@@ -12,6 +12,11 @@ import Avatar from 'soapbox/components/avatar';
import DisplayName from 'soapbox/components/display_name';
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
import PollPreview from './poll_preview';
+import PlaceholderCard from 'soapbox/features/placeholder/components/placeholder_card';
+
+const shouldHaveCard = pendingStatus => {
+ return Boolean(pendingStatus.get('content').match(/https?:\/\/\S*/));
+};
const mapStateToProps = (state, props) => {
const { idempotencyKey } = props;
@@ -24,6 +29,23 @@ const mapStateToProps = (state, props) => {
export default @connect(mapStateToProps)
class PendingStatus extends ImmutablePureComponent {
+ renderMedia = () => {
+ const { status } = this.props;
+
+ if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) {
+ return (
+
+ );
+ } else if (shouldHaveCard(status)) {
+ return ;
+ } else {
+ return null;
+ }
+ }
+
render() {
const { status, className, showThread } = this.props;
if (!status) return null;
@@ -67,11 +89,7 @@ class PendingStatus extends ImmutablePureComponent {
collapsable
/>
-
-
+ {this.renderMedia()}
{status.get('poll') && }
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
diff --git a/app/soapbox/utils/status.js b/app/soapbox/utils/status.js
new file mode 100644
index 000000000..48554ced9
--- /dev/null
+++ b/app/soapbox/utils/status.js
@@ -0,0 +1,15 @@
+export const getFirstExternalLink = status => {
+ try {
+ // Pulled from Pleroma's media parser
+ const selector = 'a:not(.mention,.hashtag,.attachment,[rel~="tag"])';
+ const element = document.createElement('div');
+ element.innerHTML = status.content;
+ return element.querySelector(selector);
+ } catch {
+ return null;
+ }
+};
+
+export const shouldHaveCard = status => {
+ return Boolean(getFirstExternalLink(status));
+};
diff --git a/app/styles/placeholder.scss b/app/styles/placeholder.scss
index 0427e9463..8366261ac 100644
--- a/app/styles/placeholder.scss
+++ b/app/styles/placeholder.scss
@@ -1,6 +1,7 @@
.placeholder-status,
.placeholder-hashtag,
-.notification--placeholder {
+.notification--placeholder,
+.status-card--placeholder {
position: relative;
&::before {
@@ -105,3 +106,16 @@
background: transparent;
box-shadow: none;
}
+
+.status-card--placeholder {
+ pointer-events: none;
+
+ .status-card__title,
+ .status-card__description,
+ .status-card__host {
+ letter-spacing: -1px;
+ color: var(--brand-color) !important;
+ word-break: break-all;
+ opacity: 0.1;
+ }
+}