Placeholder: display placeholder statuses while timelines are loading
This commit is contained in:
parent
d41e3f96ee
commit
47b433915b
11 changed files with 188 additions and 4 deletions
|
@ -29,6 +29,8 @@ export default class ScrollableList extends PureComponent {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
|
placeholderComponent: PropTypes.node,
|
||||||
|
placeholderCount: PropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -222,7 +224,13 @@ export default class ScrollableList extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoading = () => {
|
renderLoading = () => {
|
||||||
const { prepend } = this.props;
|
const { prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
|
||||||
|
|
||||||
|
if (Placeholder && placeholderCount > 0) {
|
||||||
|
return Array(placeholderCount).fill().map((_, i) => (
|
||||||
|
<Placeholder key={i} />
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='slist slist--flex'>
|
<div className='slist slist--flex'>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import LoadGap from './load_gap';
|
import LoadGap from './load_gap';
|
||||||
import ScrollableList from './scrollable_list';
|
import ScrollableList from './scrollable_list';
|
||||||
import TimelineQueueButtonHeader from './timeline_queue_button_header';
|
import TimelineQueueButtonHeader from './timeline_queue_button_header';
|
||||||
|
import PlaceholderMaterialStatus from 'soapbox/features/placeholder/components/placeholder_material_status';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
queue: { id: 'status_list.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {post} other {posts}}' },
|
||||||
|
@ -213,7 +214,16 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
count={totalQueuedItemsCount}
|
count={totalQueuedItemsCount}
|
||||||
message={messages.queue}
|
message={messages.queue}
|
||||||
/>,
|
/>,
|
||||||
<ScrollableList key='scrollable-list' {...other} isLoading={isLoading} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
|
<ScrollableList
|
||||||
|
key='scrollable-list'
|
||||||
|
isLoading={isLoading}
|
||||||
|
showLoading={isLoading && statusIds.size === 0}
|
||||||
|
onLoadMore={onLoadMore && this.handleLoadOlder}
|
||||||
|
placeholderComponent={PlaceholderMaterialStatus}
|
||||||
|
placeholderCount={20}
|
||||||
|
ref={this.setRef}
|
||||||
|
{...other}
|
||||||
|
>
|
||||||
{this.renderScrollableContent()}
|
{this.renderScrollableContent()}
|
||||||
</ScrollableList>,
|
</ScrollableList>,
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export default class Avatar extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
size: PropTypes.number,
|
||||||
|
style: PropTypes.object,
|
||||||
|
inline: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
inline: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { size, inline } = this.props;
|
||||||
|
|
||||||
|
// : TODO : remove inline and change all avatars to be sized using css
|
||||||
|
const style = !size ? {} : {
|
||||||
|
width: `${size}px`,
|
||||||
|
height: `${size}px`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames('account__avatar', { 'account__avatar-inline': inline })}
|
||||||
|
style={style}
|
||||||
|
alt=''
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { randomIntFromInterval, generateText } from '../utils';
|
||||||
|
|
||||||
|
export default class DisplayName extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
maxLength: PropTypes.number.isRequired,
|
||||||
|
minLength: PropTypes.number.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { maxLength, minLength } = this.props;
|
||||||
|
const length = randomIntFromInterval(maxLength, minLength);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='display-name'>
|
||||||
|
<span>
|
||||||
|
<span className='display-name__name'>
|
||||||
|
<bdi><strong className='display-name__html'>{generateText(length)}</strong></bdi>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PlaceholderStatus from './placeholder_status';
|
||||||
|
|
||||||
|
export default class PlaceholderMaterialStatus extends React.Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className='material-status' tabIndex={-1} aria-hidden>
|
||||||
|
<div className='material-status__status' tabIndex={0}>
|
||||||
|
<PlaceholderStatus {...this.props} focusable={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PlaceholderAvatar from './placeholder_avatar';
|
||||||
|
import PlaceholderDisplayName from './placeholder_display_name';
|
||||||
|
import PlaceholderStatusContent from './placeholder_status_content';
|
||||||
|
|
||||||
|
export default class PlaceholderStatus 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='placeholder-status'>
|
||||||
|
<div className='status__wrapper'>
|
||||||
|
<div className='status'>
|
||||||
|
<div className='status__info'>
|
||||||
|
<div className='status__profile'>
|
||||||
|
<div className='status__avatar'>
|
||||||
|
<PlaceholderAvatar size={48} />
|
||||||
|
</div>
|
||||||
|
<span className='status__display-name'>
|
||||||
|
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PlaceholderStatusContent minLength={5} maxLength={120} />
|
||||||
|
|
||||||
|
{/* TODO */}
|
||||||
|
{/* <PlaceholderActionBar /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { randomIntFromInterval, generateText } from '../utils';
|
||||||
|
|
||||||
|
export default class PlaceholderStatusContent extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
maxLength: PropTypes.number.isRequired,
|
||||||
|
minLength: PropTypes.number.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { maxLength, minLength } = this.props;
|
||||||
|
const length = randomIntFromInterval(maxLength, minLength);
|
||||||
|
|
||||||
|
return(
|
||||||
|
<div className='status__content' tabIndex='0' key='content'>
|
||||||
|
{generateText(length)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
app/soapbox/features/placeholder/utils.js
Normal file
16
app/soapbox/features/placeholder/utils.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export const PLACEHOLDER_CHAR = '█';
|
||||||
|
|
||||||
|
export const generateText = length => {
|
||||||
|
let text = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
text += PLACEHOLDER_CHAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
|
// https://stackoverflow.com/a/7228322/8811886
|
||||||
|
export const randomIntFromInterval = (min, max) => {
|
||||||
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
|
};
|
|
@ -204,3 +204,12 @@ noscript {
|
||||||
.greentext {
|
.greentext {
|
||||||
color: #789922;
|
color: #789922;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.placeholder-status {
|
||||||
|
.status__display-name strong,
|
||||||
|
.status__content {
|
||||||
|
letter-spacing: -1px;
|
||||||
|
color: var(--brand-color);
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -881,7 +881,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make MaterialStatus flush against SubNavigation
|
// Make MaterialStatus flush against SubNavigation
|
||||||
.sub-navigation ~ .slist .item-list > article:first-child .material-status__status {
|
.sub-navigation ~ .slist .item-list > article:first-child .material-status__status,
|
||||||
|
.sub-navigation ~ .material-status .material-status__status {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
.display-name__account {
|
.display-name__account {
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.display-name .profile-hover-card {
|
.display-name .profile-hover-card {
|
||||||
|
|
Loading…
Reference in a new issue