diff --git a/app/soapbox/components/scrollable_list.js b/app/soapbox/components/scrollable_list.js
index 3576960db..0e6c1991b 100644
--- a/app/soapbox/components/scrollable_list.js
+++ b/app/soapbox/components/scrollable_list.js
@@ -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) => (
+
+ ));
+ }
return (
diff --git a/app/soapbox/components/status_list.js b/app/soapbox/components/status_list.js
index f5808a648..a186a8073 100644
--- a/app/soapbox/components/status_list.js
+++ b/app/soapbox/components/status_list.js
@@ -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}
/>,
-
+
{this.renderScrollableContent()}
,
];
diff --git a/app/soapbox/features/placeholder/components/placeholder_avatar.js b/app/soapbox/features/placeholder/components/placeholder_avatar.js
new file mode 100644
index 000000000..8c9360321
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_avatar.js
@@ -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 (
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_display_name.js b/app/soapbox/features/placeholder/components/placeholder_display_name.js
new file mode 100644
index 000000000..dd5c2ad47
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_display_name.js
@@ -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 (
+
+
+
+ {generateText(length)}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_material_status.js b/app/soapbox/features/placeholder/components/placeholder_material_status.js
new file mode 100644
index 000000000..f55d9d086
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_material_status.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import PlaceholderStatus from './placeholder_status';
+
+export default class PlaceholderMaterialStatus extends React.Component {
+
+ render() {
+ return (
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_status.js b/app/soapbox/features/placeholder/components/placeholder_status.js
new file mode 100644
index 000000000..1fea821ca
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_status.js
@@ -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 (
+
+
+
+
+
+
+
+ {/* TODO */}
+ {/*
*/}
+
+
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/components/placeholder_status_content.js b/app/soapbox/features/placeholder/components/placeholder_status_content.js
new file mode 100644
index 000000000..cf47c80d1
--- /dev/null
+++ b/app/soapbox/features/placeholder/components/placeholder_status_content.js
@@ -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(
+
+ {generateText(length)}
+
+ );
+ }
+
+}
diff --git a/app/soapbox/features/placeholder/utils.js b/app/soapbox/features/placeholder/utils.js
new file mode 100644
index 000000000..f2001a61d
--- /dev/null
+++ b/app/soapbox/features/placeholder/utils.js
@@ -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);
+};
diff --git a/app/styles/basics.scss b/app/styles/basics.scss
index 4694adbfd..26f98bbf8 100644
--- a/app/styles/basics.scss
+++ b/app/styles/basics.scss
@@ -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;
+ }
+}
diff --git a/app/styles/components/columns.scss b/app/styles/components/columns.scss
index 2f9abd13b..726f9f40b 100644
--- a/app/styles/components/columns.scss
+++ b/app/styles/components/columns.scss
@@ -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;
}
diff --git a/app/styles/components/profile_hover_card.scss b/app/styles/components/profile_hover_card.scss
index a30895897..e90cd64ad 100644
--- a/app/styles/components/profile_hover_card.scss
+++ b/app/styles/components/profile_hover_card.scss
@@ -1,6 +1,5 @@
.display-name__account {
position: relative;
- cursor: pointer;
}
.display-name .profile-hover-card {