From 1714ac03d2f1980a9e5687df307797f3bb841ebb Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 19:08:27 -0600 Subject: [PATCH 1/2] Status: display a placeholder Card on own links, poll for updated card --- app/soapbox/actions/statuses.js | 24 +++++++++++++++ app/soapbox/components/status.js | 5 ++++ .../components/placeholder_card.js | 29 +++++++++++++++++++ .../features/status/components/card.js | 2 +- app/soapbox/utils/status.js | 15 ++++++++++ app/styles/placeholder.scss | 16 +++++++++- 6 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 app/soapbox/features/placeholder/components/placeholder_card.js create mode 100644 app/soapbox/utils/status.js 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/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; + } +} From a3ce7b2afe92ec4e6659c73e15cb638e90aac58e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 15 Nov 2021 21:58:30 -0600 Subject: [PATCH 2/2] PendingStatus: display PlaceholderCard when a card is expected --- .../features/ui/components/pending_status.js | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) 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']) && (