diff --git a/app/javascript/mastodon/components/followed_tags_list.jsx b/app/javascript/mastodon/components/followed_tags_list.jsx
new file mode 100644
index 00000000000..a294e6ae6c5
--- /dev/null
+++ b/app/javascript/mastodon/components/followed_tags_list.jsx
@@ -0,0 +1,67 @@
+import PropTypes from 'prop-types';
+import { PureComponent } from 'react';
+
+import { injectIntl } from 'react-intl';
+
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import { connect } from 'react-redux';
+
+import debounce from 'lodash/debounce';
+
+import {
+ expandFollowedHashtags,
+ fetchFollowedHashtags,
+} from 'mastodon/actions/tags';
+import ButtonScrollList from 'mastodon/components/button_scroll_list';
+import { Hashtag } from 'mastodon/components/hashtag';
+import { WithRouterPropTypes } from 'mastodon/utils/react_router';
+
+const mapStateToProps = (state) => ({
+ hashtags: state.getIn(['followed_tags', 'items']),
+ isLoading: state.getIn(['followed_tags', 'isLoading'], true),
+ hasMore: !!state.getIn(['followed_tags', 'next']),
+});
+
+class FollowedTagsList extends PureComponent {
+ static propTypes = {
+ hashtags: ImmutablePropTypes.list.isRequired,
+ isLoading: PropTypes.bool.isRequired,
+ hasMore: PropTypes.bool.isRequired,
+ ...WithRouterPropTypes,
+ };
+
+ componentDidMount() {
+ this.props.dispatch(fetchFollowedHashtags());
+ }
+
+ handleLoadMore = debounce(
+ () => {
+ this.props.dispatch(expandFollowedHashtags());
+ },
+ 300,
+ { leading: true },
+ );
+
+ render() {
+ const { hashtags } = this.props;
+
+ return (
+
+
+ {hashtags.map((hashtag) => (
+
+
+
+ ))}
+
+
+ );
+ }
+}
+
+export default connect(mapStateToProps)(injectIntl(FollowedTagsList));
diff --git a/app/javascript/mastodon/components/hashtag.tsx b/app/javascript/mastodon/components/hashtag.tsx
index 8963e4a40d8..585d2b27cf1 100644
--- a/app/javascript/mastodon/components/hashtag.tsx
+++ b/app/javascript/mastodon/components/hashtag.tsx
@@ -85,6 +85,7 @@ export interface HashtagProps {
description?: React.ReactNode;
history?: number[];
name: string;
+ showSkeleton?: boolean;
people: number;
to: string;
uses?: number;
@@ -93,6 +94,7 @@ export interface HashtagProps {
export const Hashtag: React.FC
= ({
name,
+ showSkeleton = true,
to,
people,
uses,
@@ -113,13 +115,15 @@ export const Hashtag: React.FC = ({
)}
- {description ? (
- {description}
- ) : typeof people !== 'undefined' ? (
-
- ) : (
-
- )}
+ {showSkeleton ? (
+ description ? (
+ {description}
+ ) : typeof people !== 'undefined' ? (
+
+ ) : (
+
+ )
+ ) : null}
{typeof uses !== 'undefined' && (
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index cbf9314ff83..12b522e85f3 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -3571,6 +3571,42 @@ $ui-header-logo-wordmark-width: 99px;
}
}
+.button-scroll-list-container {
+ display: flex;
+ align-items: center;
+ max-width: 100%;
+ overflow: hidden;
+}
+
+.icon-button {
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 0;
+}
+
+.button-scroll-list {
+ display: flex;
+ overflow-x: auto;
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+ flex-grow: 1;
+ white-space: nowrap;
+}
+
+.button-scroll-list::-webkit-scrollbar {
+ display: none;
+}
+
+.followed-tags-list {
+ overflow: hidden;
+ flex: 0 1 auto;
+}
+
+.hashtag-wrapper {
+ border-bottom: none;
+}
+
.column-back-button {
box-sizing: border-box;
width: 100%;