Merge branch 'card-placeholder' into 'develop'
Status: display a placeholder Card on own links, poll for updated card See merge request soapbox-pub/soapbox-fe!874
This commit is contained in:
commit
0f2d464182
|
@ -3,6 +3,7 @@ import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||||
import { openModal } from './modal';
|
import { openModal } from './modal';
|
||||||
import { isLoggedIn } from 'soapbox/utils/auth';
|
import { isLoggedIn } from 'soapbox/utils/auth';
|
||||||
|
import { shouldHaveCard } from 'soapbox/utils/status';
|
||||||
|
|
||||||
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
export const STATUS_CREATE_REQUEST = 'STATUS_CREATE_REQUEST';
|
||||||
export const STATUS_CREATE_SUCCESS = 'STATUS_CREATE_SUCCESS';
|
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, {
|
return api(getState).post('/api/v1/statuses', params, {
|
||||||
headers: { 'Idempotency-Key': idempotencyKey },
|
headers: { 'Idempotency-Key': idempotencyKey },
|
||||||
}).then(({ data: status }) => {
|
}).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(importFetchedStatus(status, idempotencyKey));
|
||||||
dispatch({ type: STATUS_CREATE_SUCCESS, status, params, 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;
|
return status;
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
|
dispatch({ type: STATUS_CREATE_FAIL, error, params, idempotencyKey });
|
||||||
|
|
|
@ -19,6 +19,7 @@ import Icon from 'soapbox/components/icon';
|
||||||
import { Link, NavLink } from 'react-router-dom';
|
import { Link, NavLink } from 'react-router-dom';
|
||||||
import { getDomain } from 'soapbox/utils/accounts';
|
import { getDomain } from 'soapbox/utils/accounts';
|
||||||
import HoverRefWrapper from 'soapbox/components/hover_ref_wrapper';
|
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
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
|
@ -465,6 +466,10 @@ class Status extends ImmutablePureComponent {
|
||||||
defaultWidth={this.props.cachedMediaWidth}
|
defaultWidth={this.props.cachedMediaWidth}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (status.get('expectsCard', false)) {
|
||||||
|
media = (
|
||||||
|
<PlaceholderCard />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherAccounts && otherAccounts.size > 1) {
|
if (otherAccounts && otherAccounts.size > 1) {
|
||||||
|
|
|
@ -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 (
|
||||||
|
<div className='status-card status-card--placeholder'>
|
||||||
|
<div className='status-card__image' />
|
||||||
|
<div className='status-card__content'>
|
||||||
|
<span className='status-card__title'>{generateText(randomIntFromInterval(5, 25))}</span>
|
||||||
|
<p className='status-card__description'>
|
||||||
|
{generateText(randomIntFromInterval(5, 75))}
|
||||||
|
</p>
|
||||||
|
<span className='status-card__host'>
|
||||||
|
{generateText(randomIntFromInterval(5, 15))}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -171,7 +171,7 @@ export default class Card extends React.PureComponent {
|
||||||
|
|
||||||
const description = (
|
const description = (
|
||||||
<div className='status-card__content'>
|
<div className='status-card__content'>
|
||||||
{title}
|
<span className='status-card__title'>{title}</span>
|
||||||
<p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>
|
<p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>
|
||||||
<span className='status-card__host'><Icon src={require('@tabler/icons/icons/link.svg')} /> {provider}</span>
|
<span className='status-card__host'><Icon src={require('@tabler/icons/icons/link.svg')} /> {provider}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,11 @@ import Avatar from 'soapbox/components/avatar';
|
||||||
import DisplayName from 'soapbox/components/display_name';
|
import DisplayName from 'soapbox/components/display_name';
|
||||||
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||||
import PollPreview from './poll_preview';
|
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 mapStateToProps = (state, props) => {
|
||||||
const { idempotencyKey } = props;
|
const { idempotencyKey } = props;
|
||||||
|
@ -24,6 +29,23 @@ const mapStateToProps = (state, props) => {
|
||||||
export default @connect(mapStateToProps)
|
export default @connect(mapStateToProps)
|
||||||
class PendingStatus extends ImmutablePureComponent {
|
class PendingStatus extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
renderMedia = () => {
|
||||||
|
const { status } = this.props;
|
||||||
|
|
||||||
|
if (status.get('media_attachments') && !status.get('media_attachments').isEmpty()) {
|
||||||
|
return (
|
||||||
|
<AttachmentThumbs
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (shouldHaveCard(status)) {
|
||||||
|
return <PlaceholderCard />;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { status, className, showThread } = this.props;
|
const { status, className, showThread } = this.props;
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
|
@ -67,11 +89,7 @@ class PendingStatus extends ImmutablePureComponent {
|
||||||
collapsable
|
collapsable
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<AttachmentThumbs
|
{this.renderMedia()}
|
||||||
compact
|
|
||||||
media={status.get('media_attachments')}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{status.get('poll') && <PollPreview poll={status.get('poll')} />}
|
{status.get('poll') && <PollPreview poll={status.get('poll')} />}
|
||||||
|
|
||||||
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
|
||||||
|
|
|
@ -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));
|
||||||
|
};
|
|
@ -1,6 +1,7 @@
|
||||||
.placeholder-status,
|
.placeholder-status,
|
||||||
.placeholder-hashtag,
|
.placeholder-hashtag,
|
||||||
.notification--placeholder {
|
.notification--placeholder,
|
||||||
|
.status-card--placeholder {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -105,3 +106,16 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
box-shadow: none;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue