diff --git a/app/javascript/mastodon/components/gallery.js b/app/javascript/mastodon/components/gallery.js
new file mode 100644
index 000000000..ccaa7693d
--- /dev/null
+++ b/app/javascript/mastodon/components/gallery.js
@@ -0,0 +1,133 @@
+import { debounce } from 'lodash';
+import React from 'react';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import StatusContainer from '../containers/status_container';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import LoadGap from './load_gap';
+import ScrollableList from './scrollable_list';
+import RegenerationIndicator from 'mastodon/components/regeneration_indicator';
+
+export default class Gallery extends ImmutablePureComponent {
+
+ static propTypes = {
+ scrollKey : PropTypes.string.isRequired,
+ statusIds : ImmutablePropTypes.list.isRequired,
+ featuredStatusIds : ImmutablePropTypes.list,
+ onLoadMore : PropTypes.func,
+ onScrollToTop : PropTypes.func,
+ onScroll : PropTypes.func,
+ trackScroll : PropTypes.bool,
+ shouldUpdateScroll: PropTypes.func,
+ isLoading : PropTypes.bool,
+ isPartial : PropTypes.bool,
+ hasMore : PropTypes.bool,
+ prepend : PropTypes.node,
+ emptyMessage : PropTypes.node,
+ alwaysPrepend : PropTypes.bool,
+ timelineId : PropTypes.string,
+ };
+
+ static defaultProps = {
+ trackScroll: true,
+ };
+
+ getFeaturedStatusCount = () => {
+ return this.props.featuredStatusIds ? this.props.featuredStatusIds.size : 0;
+ };
+
+ getCurrentStatusIndex = (id, featured) => {
+ if (featured) {
+ return this.props.featuredStatusIds.indexOf(id);
+ } else {
+ return this.props.statusIds.indexOf(id) + this.getFeaturedStatusCount();
+ }
+ };
+
+ handleMoveUp = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) - 1;
+ this._selectChild(elementIndex, true);
+ };
+
+ handleMoveDown = (id, featured) => {
+ const elementIndex = this.getCurrentStatusIndex(id, featured) + 1;
+ this._selectChild(elementIndex, false);
+ };
+
+ handleLoadOlder = debounce(() => {
+ this.props.onLoadMore(this.props.statusIds.size > 0 ? this.props.statusIds.last() : undefined);
+ }, 300, { leading: true });
+
+ _selectChild(index, align_top) {
+ const container = this.node.node;
+ const element = container.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
+
+ if (element) {
+ if (align_top && container.scrollTop > element.offsetTop) {
+ element.scrollIntoView(true);
+ } else if (!align_top && container.scrollTop + container.clientHeight < element.offsetTop + element.offsetHeight) {
+ element.scrollIntoView(false);
+ }
+ element.focus();
+ }
+ }
+
+ setRef = c => {
+ this.node = c;
+ };
+
+ render() {
+ const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other } = this.props;
+ const { isLoading, isPartial } = other;
+
+ if (isPartial) {
+ return ;
+ }
+
+ let scrollableContent = (isLoading || statusIds.size > 0) ? (
+ statusIds.map((statusId, index) => statusId === null ? (
+ 0 ? statusIds.get(index - 1) : null}
+ onClick={onLoadMore}
+ />
+ ) : (
+
+ ))
+ ) : null;
+
+ if (scrollableContent && featuredStatusIds) {
+ scrollableContent = featuredStatusIds.map(statusId => (
+
+ )).concat(scrollableContent);
+ }
+
+ return (
+
+ {scrollableContent}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/containers/gallery_container.js b/app/javascript/mastodon/containers/gallery_container.js
new file mode 100644
index 000000000..7d23a42e3
--- /dev/null
+++ b/app/javascript/mastodon/containers/gallery_container.js
@@ -0,0 +1,43 @@
+import initialState from '../initial_state';
+import React, { Fragment } from 'react';
+import configureStore from '../store/configureStore';
+
+import { Provider } from 'react-redux';
+
+import PropTypes from 'prop-types';
+import { hydrateStore } from '../actions/store';
+import { addLocaleData, IntlProvider } from 'react-intl';
+import { getLocale } from '../locales';
+
+const { localeData, messages } = getLocale();
+addLocaleData(localeData);
+
+const store = configureStore();
+
+if (initialState) {
+ store.dispatch(hydrateStore(initialState));
+}
+
+export default class GalleryContainer extends React.PureComponent {
+ static propTypes = {
+ locale: PropTypes.string.isRequired,
+ };
+
+ render() {
+ const { locale } = this.props;
+ return (
+
+
+
+
+
+
+
+
+
+ );
+ }
+}