diff --git a/app/soapbox/components/filter_bar.js b/app/soapbox/components/filter_bar.js
new file mode 100644
index 000000000..a2ba02775
--- /dev/null
+++ b/app/soapbox/components/filter_bar.js
@@ -0,0 +1,156 @@
+import React from 'react';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import { debounce } from 'lodash';
+
+export default class FilterBar extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ items: PropTypes.array.isRequired,
+ active: PropTypes.string,
+ className: PropTypes.string,
+ };
+
+ state = {
+ mounted: false,
+ };
+
+ componentDidMount() {
+ document.addEventListener('keydown', this.handleKeyDown, false);
+ window.addEventListener('resize', this.handleResize, { passive: true });
+
+ const { left, width } = this.getActiveTabIndicationSize();
+ this.setState({ mounted: true, left, width });
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('keydown', this.handleKeyDown, false);
+ document.removeEventListener('resize', this.handleResize, false);
+ }
+
+ handleResize = debounce(() => {
+ this.setState(this.getActiveTabIndicationSize());
+ }, 300, {
+ trailing: true,
+ });
+
+ componentDidUpdate(prevProps) {
+ if (this.props.active !== prevProps.active) {
+ this.setState(this.getActiveTabIndicationSize());
+ }
+ }
+
+ setRef = c => {
+ this.node = c;
+ }
+
+ setFocusRef = c => {
+ this.focusedItem = c;
+ }
+
+ handleKeyDown = e => {
+ const items = Array.from(this.node.getElementsByTagName('a'));
+ const index = items.indexOf(document.activeElement);
+ let element = null;
+
+ switch(e.key) {
+ case 'ArrowRight':
+ element = items[index+1] || items[0];
+ break;
+ case 'ArrowLeft':
+ element = items[index-1] || items[items.length-1];
+ break;
+ }
+
+ if (element) {
+ element.focus();
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+
+ handleItemKeyPress = e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ this.handleClick(e);
+ }
+ }
+
+ handleClick = e => {
+ const i = Number(e.currentTarget.getAttribute('data-index'));
+ const { action, to } = this.props.items[i];
+
+ if (typeof action === 'function') {
+ e.preventDefault();
+ action(e);
+ } else if (to) {
+ e.preventDefault();
+ this.context.router.history.push(to);
+ }
+ }
+
+ getActiveTabIndicationSize() {
+ const { active, items } = this.props;
+
+ if (!active || !this.node) return { width: null };
+
+ const index = items.findIndex(({ name }) => name === active);
+ const elements = Array.from(this.node.getElementsByTagName('a'));
+ const element = elements[index];
+
+ if (!element) return { width: null };
+
+ const left = element.offsetLeft;
+ const { width } = element.getBoundingClientRect();
+
+ return { left, width };
+ }
+
+ renderActiveTabIndicator() {
+ const { left, width } = this.state;
+
+ return (
+
+ );
+ }
+
+ renderItem(option, i) {
+ if (option === null) {
+ return ;
+ }
+
+ const { name, text, href, to, title } = option;
+
+ return (
+
+ {text}
+
+ );
+ }
+
+ render() {
+ const { className, items } = this.props;
+ const { mounted } = this.state;
+
+ return (
+
+ {mounted && this.renderActiveTabIndicator()}
+ {items.map((option, i) => this.renderItem(option, i))}
+
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/app/soapbox/features/compose/components/search_results.js b/app/soapbox/features/compose/components/search_results.js
index e316123b1..13487887b 100644
--- a/app/soapbox/features/compose/components/search_results.js
+++ b/app/soapbox/features/compose/components/search_results.js
@@ -2,18 +2,26 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import ImmutablePropTypes from 'react-immutable-proptypes';
+import { defineMessages, injectIntl } from 'react-intl';
import AccountContainer from '../../../containers/account_container';
import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag';
-import FilterBar from '../../search/components/filter_bar';
import ScrollableList from 'soapbox/components/scrollable_list';
import PlaceholderAccount from 'soapbox/features/placeholder/components/placeholder_account';
import PlaceholderHashtag from 'soapbox/features/placeholder/components/placeholder_hashtag';
import PlaceholderStatus from 'soapbox/features/placeholder/components/placeholder_status';
import Pullable from 'soapbox/components/pullable';
+import FilterBar from 'soapbox/components/filter_bar';
-export default class SearchResults extends ImmutablePureComponent {
+const messages = defineMessages({
+ accounts: { id: 'search_results.accounts', defaultMessage: 'People' },
+ statuses: { id: 'search_results.statuses', defaultMessage: 'Posts' },
+ hashtags: { id: 'search_results.hashtags', defaultMessage: 'Hashtags' },
+});
+
+export default @injectIntl
+class SearchResults extends ImmutablePureComponent {
static propTypes = {
value: PropTypes.string,
@@ -25,12 +33,37 @@ export default class SearchResults extends ImmutablePureComponent {
features: PropTypes.object.isRequired,
suggestions: ImmutablePropTypes.list,
trends: ImmutablePropTypes.list,
+ intl: PropTypes.object.isRequired,
};
handleLoadMore = () => this.props.expandSearch(this.props.selectedFilter);
handleSelectFilter = newActiveFilter => this.props.selectFilter(newActiveFilter);
+ renderFilterBar() {
+ const { intl, selectedFilter } = this.props;
+
+ const items = [
+ {
+ text: intl.formatMessage(messages.accounts),
+ action: () => this.handleSelectFilter('accounts'),
+ name: 'accounts',
+ },
+ {
+ text: intl.formatMessage(messages.statuses),
+ action: () => this.handleSelectFilter('statuses'),
+ name: 'statuses',
+ },
+ {
+ text: intl.formatMessage(messages.hashtags),
+ action: () => this.handleSelectFilter('hashtags'),
+ name: 'hashtags',
+ },
+ ];
+
+ return ;
+ }
+
render() {
const { value, results, submitted, selectedFilter, suggestions, trends } = this.props;
@@ -105,7 +138,7 @@ export default class SearchResults extends ImmutablePureComponent {
return (
<>
-
+ {this.renderFilterBar()}
{noResultsMessage || (
diff --git a/app/soapbox/features/notifications/components/filter_bar.js b/app/soapbox/features/notifications/components/filter_bar.js
index f0802f66b..33d4f41f7 100644
--- a/app/soapbox/features/notifications/components/filter_bar.js
+++ b/app/soapbox/features/notifications/components/filter_bar.js
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
+import { defineMessages, injectIntl } from 'react-intl';
import Icon from 'soapbox/components/icon';
+import FilterBar from 'soapbox/components/filter_bar';
-const tooltips = defineMessages({
+const messages = defineMessages({
+ all: { id: 'notifications.filter.all', defaultMessage: 'All' },
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Likes' },
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Reposts' },
@@ -14,7 +16,7 @@ const tooltips = defineMessages({
});
export default @injectIntl
-class FilterBar extends React.PureComponent {
+class NotificationFilterBar extends React.PureComponent {
static propTypes = {
selectFilter: PropTypes.func.isRequired,
@@ -30,90 +32,67 @@ class FilterBar extends React.PureComponent {
render() {
const { selectedFilter, advancedMode, supportsEmojiReacts, intl } = this.props;
- const renderedElement = !advancedMode ? (
-
-
-
-
- ) : (
-
-
-
-
- {supportsEmojiReacts && }
-
-
-
-
-
- );
- return renderedElement;
+
+ const items = [
+ {
+ text: intl.formatMessage(messages.all),
+ action: this.onClick('all'),
+ name: 'all',
+ },
+ ];
+
+ if (!advancedMode) {
+ items.push({
+ text: intl.formatMessage(messages.mentions),
+ action: this.onClick('mention'),
+ name: 'mention',
+ });
+ } else {
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.mentions),
+ action: this.onClick('mention'),
+ name: 'mention',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.favourites),
+ action: this.onClick('favourite'),
+ name: 'favourite',
+ });
+ if (supportsEmojiReacts) items.push({
+ text: ,
+ title: intl.formatMessage(messages.emoji_reacts),
+ action: this.onClick('pleroma:emoji_reaction'),
+ name: 'pleroma:emoji_reaction',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.boosts),
+ action: this.onClick('reblog'),
+ name: 'reblog',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.polls),
+ action: this.onClick('poll'),
+ name: 'poll',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.follows),
+ action: this.onClick('follow'),
+ name: 'follow',
+ });
+ items.push({
+ text: ,
+ title: intl.formatMessage(messages.moves),
+ action: this.onClick('move'),
+ name: 'move',
+ });
+ }
+
+ return ;
}
}
diff --git a/app/soapbox/features/search/components/filter_bar.js b/app/soapbox/features/search/components/filter_bar.js
deleted file mode 100644
index 917ad99c7..000000000
--- a/app/soapbox/features/search/components/filter_bar.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { FormattedMessage, injectIntl } from 'react-intl';
-
-export default @injectIntl
-class FilterBar extends React.PureComponent {
-
- static propTypes = {
- selectFilter: PropTypes.func.isRequired,
- selectedFilter: PropTypes.string.isRequired,
- };
-
- onClick(searchType) {
- return () => this.props.selectFilter(searchType);
- }
-
- render() {
- const { selectedFilter } = this.props;
-
- return (
-
-
-
-
-
- );
- }
-
-}
diff --git a/app/soapbox/features/ui/components/reactions_modal.js b/app/soapbox/features/ui/components/reactions_modal.js
index 8b905ac25..3f7617b1f 100644
--- a/app/soapbox/features/ui/components/reactions_modal.js
+++ b/app/soapbox/features/ui/components/reactions_modal.js
@@ -9,9 +9,11 @@ import LoadingIndicator from 'soapbox/components/loading_indicator';
import AccountContainer from 'soapbox/containers/account_container';
import ScrollableList from 'soapbox/components/scrollable_list';
import { fetchFavourites, fetchReactions } from 'soapbox/actions/interactions';
+import FilterBar from 'soapbox/components/filter_bar';
const messages = defineMessages({
close: { id: 'lightbox.close', defaultMessage: 'Close' },
+ all: { id: 'reactions.all', defaultMessage: 'All' },
});
const mapStateToProps = (state, props) => {
@@ -62,6 +64,29 @@ class ReactionsModal extends React.PureComponent {
this.setState({ reaction });
};
+ renderFilterBar() {
+ const { intl, reactions } = this.props;
+ const { reaction } = this.state;
+
+ const items = [
+ {
+ text: intl.formatMessage(messages.all),
+ action: this.handleFilterChange(''),
+ name: 'all',
+ },
+ ];
+
+ reactions.forEach(reaction => items.push(
+ {
+ text: `${reaction.name} ${reaction.count}`,
+ action: this.handleFilterChange(reaction.name),
+ name: reaction.name,
+ },
+ ));
+
+ return ;
+ }
+
render() {
const { intl, reactions } = this.props;
const { reaction } = this.state;
@@ -78,14 +103,7 @@ class ReactionsModal extends React.PureComponent {
const emptyMessage = ;
body = (<>
- {
- reactions.size > 0 && (
-
-
- {reactions?.filter(reaction => reaction.count).map(reaction => )}
-
- )
- }
+ {reactions.size > 0 && this.renderFilterBar()}
.domain {
}
}
-.notification__filter-bar,
-.search__filter-bar,
-.account__section-headline,
-.reaction__filter-bar {
+.filter-bar,
+.account__section-headline {
border-bottom: 1px solid var(--brand-color--faint);
cursor: default;
display: flex;
@@ -684,15 +682,30 @@ article:last-child > .domain {
background-color: var(--accent-color);
}
}
- }
- button .svg-icon {
- width: 18px;
- height: 18px;
- margin: 0 auto;
+ .svg-icon {
+ width: 18px;
+ height: 18px;
+ margin: 0 auto;
+ }
}
}
+.filter-bar {
+ position: relative;
+
+ &__active {
+ position: absolute;
+ height: 3px;
+ bottom: 0;
+ background-color: var(--accent-color);
+ }
+}
+
+.no-reduce-motion .filter-bar__active {
+ transition: all 0.3s;
+}
+
.reaction__filter-bar {
overflow-x: auto;
overflow-y: hidden;