Added placeholder loaders to multiple components

• Added:
- placeholder loaders to multiple components
- status, panels, comment, lists, group items, notifications
This commit is contained in:
mgabdev 2020-07-28 15:11:51 -05:00
parent a38d9f6133
commit bc6cf0e624
12 changed files with 141 additions and 68 deletions

View File

@ -4,6 +4,7 @@ import Block from './block'
import ScrollableList from './scrollable_list' import ScrollableList from './scrollable_list'
import ListItem from './list_item' import ListItem from './list_item'
import Dummy from './dummy' import Dummy from './dummy'
import ListItemPlaceholder from './placeholder/list_item_placeholder'
export default class List extends ImmutablePureComponent { export default class List extends ImmutablePureComponent {
@ -45,6 +46,8 @@ export default class List extends ImmutablePureComponent {
scrollKey={scrollKey} scrollKey={scrollKey}
emptyMessage={emptyMessage} emptyMessage={emptyMessage}
showLoading={showLoading} showLoading={showLoading}
placeholderComponent={ListItemPlaceholder}
placeholderCount={6}
> >
{ {
items.map((item, i) => ( items.map((item, i) => (

View File

@ -5,6 +5,7 @@ import { fetchGroups } from '../../actions/groups'
import PanelLayout from './panel_layout' import PanelLayout from './panel_layout'
import GroupListItem from '../group_list_item' import GroupListItem from '../group_list_item'
import ScrollableList from '../scrollable_list' import ScrollableList from '../scrollable_list'
import GroupListItemPlaceholder from '../placeholder/group_list_item_placeholder'
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'groups.sidebar-panel.title', defaultMessage: 'Groups you\'re in' }, title: { id: 'groups.sidebar-panel.title', defaultMessage: 'Groups you\'re in' },
@ -86,6 +87,8 @@ class GroupSidebarPanel extends ImmutablePureComponent {
<ScrollableList <ScrollableList
scrollKey='groups_panel' scrollKey='groups_panel'
showLoading={!fetched} showLoading={!fetched}
placeholderComponent={GroupListItemPlaceholder}
placeholderCount={6}
> >
{ {
groupIds && groupIds.slice(0, maxCount).map((groupId, i) => ( groupIds && groupIds.slice(0, maxCount).map((groupId, i) => (

View File

@ -5,6 +5,7 @@ import { expandAccountMediaTimeline } from '../../actions/timelines'
import { getAccountGallery } from '../../selectors' import { getAccountGallery } from '../../selectors'
import PanelLayout from './panel_layout' import PanelLayout from './panel_layout'
import MediaItem from '../media_item' import MediaItem from '../media_item'
import MediaGalleryPanelPlaceholder from '../placeholder/media_gallery_panel_placeholder'
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'media_gallery_panel.title', defaultMessage: 'Media' }, title: { id: 'media_gallery_panel.title', defaultMessage: 'Media' },
@ -16,6 +17,7 @@ const mapStateToProps = (state, { account }) => {
return { return {
accountId, accountId,
isLoading: state.getIn(['timelines', `account:${accountId}:media`, 'isLoading'], true),
attachments: getAccountGallery(state, accountId), attachments: getAccountGallery(state, accountId),
} }
} }
@ -29,6 +31,7 @@ class MediaGalleryPanel extends ImmutablePureComponent {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
accountId: PropTypes.string, accountId: PropTypes.string,
account: ImmutablePropTypes.map, account: ImmutablePropTypes.map,
isLoading: PropTypes.bool,
attachments: ImmutablePropTypes.list.isRequired, attachments: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
} }
@ -49,35 +52,40 @@ class MediaGalleryPanel extends ImmutablePureComponent {
render() { render() {
const { const {
intl,
account, account,
attachments attachments,
intl,
isLoading,
} = this.props } = this.props
if (!account || !attachments) return null if (!attachments) return null
return ( return (
<PanelLayout <PanelLayout
noPadding noPadding
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
headerButtonTitle={intl.formatMessage(messages.show_all)} headerButtonTitle={!!account ? intl.formatMessage(messages.show_all) : undefined}
headerButtonTo={`/${account.get('acct')}/media`} headerButtonTo={!!account ? `/${account.get('acct')}/media` : undefined}
> >
{ <div className={[_s.default, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}>
attachments.size > 0 && {
<div className={[_s.default, _s.flexRow, _s.flexWrap, _s.px10, _s.py10].join(' ')}> !!account && attachments.size > 0 &&
{ attachments.slice(0, 16).map((attachment, i) => (
attachments.slice(0, 16).map((attachment, i) => ( <MediaItem
<MediaItem isSmall
isSmall key={attachment.get('id')}
key={attachment.get('id')} attachment={attachment}
attachment={attachment} account={account}
account={account} />
/> ))
)) }
} {
</div> !account || (attachments.size === 0 && isLoading) &&
} <div className={[_s.default, _s.width100PC].join(' ')}>
<MediaGalleryPanelPlaceholder />
</div>
}
</div>
</PanelLayout> </PanelLayout>
) )
} }

View File

@ -9,6 +9,7 @@ import Divider from '../divider'
import Icon from '../icon' import Icon from '../icon'
import Text from '../text' import Text from '../text'
import Dummy from '../dummy' import Dummy from '../dummy'
import ProfileInfoPanelPlaceholder from '../placeholder/profile_info_panel_placeholder'
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'about', defaultMessage: 'About' }, title: { id: 'about', defaultMessage: 'About' },
@ -35,7 +36,15 @@ class ProfileInfoPanel extends ImmutablePureComponent {
noPanel noPanel
} = this.props } = this.props
if (!account) return null const Wrapper = noPanel ? Dummy : PanelLayout
if (!account) {
return (
<Wrapper title={intl.formatMessage(messages.title)}>
<ProfileInfoPanelPlaceholder />
</Wrapper>
)
}
const fields = account.get('fields') const fields = account.get('fields')
const content = { __html: account.get('note_emojified') } const content = { __html: account.get('note_emojified') }
@ -46,8 +55,6 @@ class ProfileInfoPanel extends ImmutablePureComponent {
const isInvestor = account.get('is_investor') const isInvestor = account.get('is_investor')
const hasBadges = isPro || isDonor || isInvestor const hasBadges = isPro || isDonor || isInvestor
const Wrapper = noPanel ? Dummy : PanelLayout
return ( return (
<Wrapper title={intl.formatMessage(messages.title)}> <Wrapper title={intl.formatMessage(messages.title)}>
<div className={[_s.default].join(' ')}> <div className={[_s.default].join(' ')}>

View File

@ -6,6 +6,7 @@ import { shortNumberFormat } from '../../utils/numbers'
import PanelLayout from './panel_layout' import PanelLayout from './panel_layout'
import UserStat from '../user_stat' import UserStat from '../user_stat'
import Dummy from '../dummy' import Dummy from '../dummy'
import ProfileStatsPanelPlaceholder from '../placeholder/profile_stats_panel_placeholder'
import ResponsiveClassesComponent from '../../features/ui/util/responsive_classes_component' import ResponsiveClassesComponent from '../../features/ui/util/responsive_classes_component'
const messages = defineMessages({ const messages = defineMessages({
@ -32,12 +33,14 @@ class ProfileStatsPanel extends ImmutablePureComponent {
noPanel noPanel
} = this.props } = this.props
if (!account) return null
const Wrapper = noPanel ? Dummy : PanelLayout const Wrapper = noPanel ? Dummy : PanelLayout
return ( return (
<Wrapper> <Wrapper>
{
!account &&
<ProfileStatsPanelPlaceholder />
}
{ {
!!account && !!account &&
<ResponsiveClassesComponent <ResponsiveClassesComponent

View File

@ -5,6 +5,7 @@ import { fetchGabTrends } from '../../actions/gab_trends'
import PanelLayout from './panel_layout' import PanelLayout from './panel_layout'
import ScrollableList from '../scrollable_list' import ScrollableList from '../scrollable_list'
import TrendsItem from '../trends_item' import TrendsItem from '../trends_item'
import TrendsItemPlaceholder from '../placeholder/trends_item_placeholder'
const messages = defineMessages({ const messages = defineMessages({
title: { id: 'trends.title', defaultMessage: 'Trending right now' }, title: { id: 'trends.title', defaultMessage: 'Trending right now' },
@ -59,7 +60,9 @@ class TrendsPanel extends ImmutablePureComponent {
title={intl.formatMessage(messages.title)} title={intl.formatMessage(messages.title)}
> >
<ScrollableList <ScrollableList
isLoading={isLoading} showLoading={isLoading}
placeholderComponent={TrendsItemPlaceholder}
placeholderCount={8}
scrollKey='trending-items' scrollKey='trending-items'
> >
{ {

View File

@ -5,7 +5,8 @@ import {
} from '../../actions/suggestions' } from '../../actions/suggestions'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import Account from '../../components/account' import Account from '../account'
import AccountPlaceholder from '../placeholder/account_placeholder'
import PanelLayout from './panel_layout' import PanelLayout from './panel_layout'
const messages = defineMessages({ const messages = defineMessages({
@ -16,6 +17,7 @@ const messages = defineMessages({
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
suggestions: state.getIn(['suggestions', 'related', 'items']), suggestions: state.getIn(['suggestions', 'related', 'items']),
isLoading: state.getIn(['suggestions', 'related', 'isLoading']),
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@ -33,6 +35,7 @@ class WhoToFollowPanel extends ImmutablePureComponent {
fetchRelatedSuggestions: PropTypes.func.isRequired, fetchRelatedSuggestions: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list.isRequired, suggestions: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
isLazy: PropTypes.bool, isLazy: PropTypes.bool,
} }
@ -69,12 +72,16 @@ class WhoToFollowPanel extends ImmutablePureComponent {
render() { render() {
const { const {
intl, intl,
isLoading,
suggestions, suggestions,
dismissRelatedSuggestion, dismissRelatedSuggestion,
} = this.props } = this.props
if (suggestions.isEmpty()) return null if (suggestions.isEmpty()) return null
const Child = isLoading ? AccountPlaceholder : Account
const arr = isLoading ? Array.apply(null, { length: 6 }) : suggestions
return ( return (
<PanelLayout <PanelLayout
noPadding noPadding
@ -84,8 +91,8 @@ class WhoToFollowPanel extends ImmutablePureComponent {
> >
<div className={_s.default}> <div className={_s.default}>
{ {
suggestions.map(accountId => ( arr.map((accountId) => (
<Account <Child
compact compact
showDismiss showDismiss
key={accountId} key={accountId}

View File

@ -1,3 +1,4 @@
import { Fragment } from 'react'
import throttle from 'lodash.throttle' import throttle from 'lodash.throttle'
import { List as ImmutableList } from 'immutable' import { List as ImmutableList } from 'immutable'
import IntersectionObserverArticle from './intersection_observer_article' import IntersectionObserverArticle from './intersection_observer_article'
@ -28,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.node,
} }
state = { state = {
@ -220,13 +223,34 @@ export default class ScrollableList extends PureComponent {
hasMore, hasMore,
emptyMessage, emptyMessage,
onLoadMore, onLoadMore,
placeholderComponent: Placeholder,
placeholderCount,
} = this.props } = this.props
const childrenCount = React.Children.count(children); const childrenCount = React.Children.count(children);
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null
if (showLoading) { if (showLoading) {
return <ColumnIndicator type='loading' /> if (Placeholder) {
return (
<Fragment>
{
Array.apply(null, {
length: placeholderCount
}).map((_, i) => (
<Placeholder
key={`${scrollKey}-placeholder-${i}`}
isLast={i === placeholderCount - 1}
/>
))
}
</Fragment>
)
}
return (
<ColumnIndicator type='loading' />
)
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) { } else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
return ( return (
<div onMouseMove={this.handleMouseMove}> <div onMouseMove={this.handleMouseMove}>

View File

@ -12,6 +12,7 @@ import { me, displayMedia } from '../initial_state'
import scheduleIdleTask from '../utils/schedule_idle_task' import scheduleIdleTask from '../utils/schedule_idle_task'
import ComposeFormContainer from '../features/compose/containers/compose_form_container' import ComposeFormContainer from '../features/compose/containers/compose_form_container'
import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component' import ResponsiveClassesComponent from '../features/ui/util/responsive_classes_component'
import CommentPlaceholder from './placeholder/comment_placeholder'
import StatusContent from './status_content' import StatusContent from './status_content'
import StatusPrepend from './status_prepend' import StatusPrepend from './status_prepend'
import StatusActionBar from './status_action_bar' import StatusActionBar from './status_action_bar'
@ -413,6 +414,7 @@ class Status extends ImmutablePureComponent {
if (isComment && !ancestorStatus && !isChild) { if (isComment && !ancestorStatus && !isChild) {
// Wait to load... // Wait to load...
// return <StatusPlaceholder />
if (contextType === 'feature') { if (contextType === 'feature') {
return <ColumnIndicator type='loading' /> return <ColumnIndicator type='loading' />
} }
@ -585,46 +587,49 @@ class Status extends ImmutablePureComponent {
<ComposeFormContainer replyToId={status.get('id')} shouldCondense /> <ComposeFormContainer replyToId={status.get('id')} shouldCondense />
</ResponsiveClassesComponent> </ResponsiveClassesComponent>
} }
{ {
status.get('replies_count') > 0 && !commentsLimited && !isNotification && descendantsIds && descendantsIds.size === 0 && status.get('replies_count') > 0 && !isChild && !isNotification && !commentsLimited &&
<Fragment> <Fragment>
<div className={[_s.default, _s.mr10, _s.ml10, _s.mb10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} /> <div className={[_s.default, _s.mr10, _s.ml10, _s.mb10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} />
<ColumnIndicator type='loading' />
</Fragment>
}
{ <div className={[_s.default, _s.px15, _s.py5, _s.mb5, _s.flexRow].join(' ')}>
descendantsIds && !isChild && !isNotification && descendantsIds.size > 0 && !commentsLimited && <Text color='secondary' size='small'>
<Fragment> {intl.formatMessage(messages.sortBy)}
<div className={[_s.default, _s.mr10, _s.ml10, _s.mb10, _s.borderColorSecondary, _s.borderBottom1PX].join(' ')} /> </Text>
<Button
isText
backgroundColor='none'
color='secondary'
className={_s.ml5}
buttonRef={this.setCommentSortButtonRef}
onClick={this.handleOnCommentSortOpen}
disabled={descendantsIds.size === 0}
>
<Text color='inherit' weight='medium' size='small'>
{sortByTitle}
</Text>
</Button>
</div>
{ {
!commentsLimited && descendantsIds.size > 1 && descendantsIds.size === 0 &&
<div className={[_s.default, _s.px15, _s.py5, _s.mb5, _s.flexRow].join(' ')}> <Fragment>
<Text color='secondary' size='small'> <CommentPlaceholder />
{intl.formatMessage(messages.sortBy)} <CommentPlaceholder />
</Text> <CommentPlaceholder />
<Button </Fragment>
isText }
backgroundColor='none'
color='secondary' {
className={_s.ml5} descendantsIds.size > 0 &&
buttonRef={this.setCommentSortButtonRef} <CommentList
onClick={this.handleOnCommentSortOpen} ancestorAccountId={status.getIn(['account', 'id'])}
> commentsLimited={commentsLimited}
<Text color='inherit' weight='medium' size='small'> descendants={descendantsIds}
{sortByTitle} onViewComments={this.handleClick}
</Text> />
</Button>
</div>
} }
<CommentList
ancestorAccountId={status.getIn(['account', 'id'])}
commentsLimited={commentsLimited}
descendants={descendantsIds}
onViewComments={this.handleClick}
/>
</Fragment> </Fragment>
} }
</div> </div>

View File

@ -9,9 +9,9 @@ import { dequeueTimeline } from '../actions/timelines'
import { scrollTopTimeline } from '../actions/timelines' import { scrollTopTimeline } from '../actions/timelines'
import { fetchStatus, fetchContext } from '../actions/statuses' import { fetchStatus, fetchContext } from '../actions/statuses'
import StatusContainer from '../containers/status_container' import StatusContainer from '../containers/status_container'
import StatusPlaceholder from './placeholder/status_placeholder'
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 ColumnIndicator from './column_indicator'
const makeGetStatusIds = () => createSelector([ const makeGetStatusIds = () => createSelector([
(state, { type, id }) => state.getIn(['settings', type], ImmutableMap()), (state, { type, id }) => state.getIn(['settings', type], ImmutableMap()),
@ -228,8 +228,15 @@ class StatusList extends ImmutablePureComponent {
} = this.props } = this.props
const { fetchedContext, refreshing } = this.state const { fetchedContext, refreshing } = this.state
if (isPartial) { if (isPartial || isLoading && statusIds.size === 0) {
return <ColumnIndicator type='loading' /> return (
<Fragment>
<StatusPlaceholder />
<StatusPlaceholder />
<StatusPlaceholder />
<StatusPlaceholder />
</Fragment>
)
} }
// : hack : // : hack :

View File

@ -17,6 +17,7 @@ import ScrollableList from '../components/scrollable_list'
import TimelineQueueButtonHeader from '../components/timeline_queue_button_header' import TimelineQueueButtonHeader from '../components/timeline_queue_button_header'
import Block from '../components/block' import Block from '../components/block'
import Account from '../components/account' import Account from '../components/account'
import NotificationPlaceholder from '../components/placeholder/notification_placeholder'
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
notifications: state.getIn(['notifications', 'items']), notifications: state.getIn(['notifications', 'items']),
@ -180,6 +181,8 @@ class Notifications extends ImmutablePureComponent {
onLoadMore={this.handleLoadOlder} onLoadMore={this.handleLoadOlder}
onScrollToTop={this.handleScrollToTop} onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll} onScroll={this.handleScroll}
placeholderComponent={NotificationPlaceholder}
placeholderCount={3}
> >
{scrollableContent} {scrollableContent}
</ScrollableList> </ScrollableList>

View File

@ -6,7 +6,7 @@ import {
fetchContext, fetchContext,
} from '../actions/statuses' } from '../actions/statuses'
import StatusContainer from '../containers/status_container' import StatusContainer from '../containers/status_container'
import ColumnIndicator from '../components/column_indicator' import StatusPlaceholder from '../components/placeholder/status_placeholder'
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const statusId = props.id || props.params.statusId const statusId = props.id || props.params.statusId
@ -73,7 +73,7 @@ class Status extends ImmutablePureComponent {
const { status } = this.props const { status } = this.props
if (!status) { if (!status) {
return <ColumnIndicator type='loading' /> return <StatusPlaceholder />
} }
return ( return (