sforkowany z mirror/soapbox
Placeholder: display placeholder statuses while timelines are loading
rodzic
d41e3f96ee
commit
47b433915b
|
@ -29,6 +29,8 @@ export default class ScrollableList extends PureComponent {
|
|||
children: PropTypes.node,
|
||||
onScrollToTop: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
placeholderComponent: PropTypes.node,
|
||||
placeholderCount: PropTypes.number,
|
||||
};
|
||||
|
||||
state = {
|
||||
|
@ -222,7 +224,13 @@ export default class ScrollableList extends PureComponent {
|
|||
}
|
||||
|
||||
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 (
|
||||
<div className='slist slist--flex'>
|
||||
|
|
|
@ -9,6 +9,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
|||
import LoadGap from './load_gap';
|
||||
import ScrollableList from './scrollable_list';
|
||||
import TimelineQueueButtonHeader from './timeline_queue_button_header';
|
||||
import PlaceholderMaterialStatus from 'soapbox/features/placeholder/components/placeholder_material_status';
|
||||
|
||||
const messages = defineMessages({
|
||||
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}
|
||||
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()}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
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
|
||||
.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-right-radius: 0;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
.display-name__account {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.display-name .profile-hover-card {
|
||||
|
|
Ładowanie…
Reference in New Issue