diff --git a/app/controllers/api/v1/shortcuts_controller.rb b/app/controllers/api/v1/shortcuts_controller.rb new file mode 100644 index 00000000..76616023 --- /dev/null +++ b/app/controllers/api/v1/shortcuts_controller.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +class Api::V1::ShortcutsController < Api::BaseController + before_action :require_user! + before_action :set_shortcut, except: [:index, :create] + + def index + @shortcuts = Shortcut.where(account: current_account).limit(100) + + @onlyGroupIds = @shortcuts.select{ |s| s.shortcut_type == 'group' }.map(&:shortcut_id) + @onlyAccountIds = @shortcuts.select{ |s| s.shortcut_type == 'account' }.map(&:shortcut_id) + + @groups = Group.where(id: @onlyGroupIds, is_archived: false).limit(100) + @accounts = Account.where(id: @onlyAccountIds).without_suspended.limit(100) + + @final = @shortcuts.map do |s| + value = nil + title = nil + to = nil + image = nil + + if s.shortcut_type == 'group' + @group = @groups.detect{ |g| g.id == s.shortcut_id } + if @group.nil? + s.destroy! + else + value = REST::GroupSerializer.new(@group) + end + elsif s.shortcut_type == 'account' + @account = @accounts.detect{ |a| a.id == s.shortcut_id } + if @account.nil? + s.destroy! + else + value = REST::AccountSerializer.new(@account) + end + end + + r = { + id: s.id, + created_at: s.created_at, + shortcut_id: s.shortcut_id, + shortcut_type: s.shortcut_type, + shortcut: value, + } + r + end + + render json: @final + end + + def show + render json: @shortcut, serializer: REST::ShortcutSerializer + end + + def create + @shortcut = Shortcut.create!(shortcut_params.merge(account: current_account)) + + value = nil + if @shortcut.shortcut_type == 'group' + @group = Group.where(id: @shortcut.shortcut_id, is_archived: false).first + value = REST::GroupSerializer.new(@group) + elsif @shortcut.shortcut_type == 'account' + @account = Account.where(id: @shortcut.shortcut_id).without_suspended.first + value = REST::AccountSerializer.new(@account) + end + + r = { + id: @shortcut.id, + created_at: @shortcut.created_at, + shortcut_type: @shortcut.shortcut_type, + shortcut_id: @shortcut.shortcut_id, + shortcut: value, + } + + render json: r + + rescue ActiveRecord::RecordNotUnique + render json: { error: I18n.t('shortcuts.errors.exists') }, status: 422 + end + + def destroy + @shortcut.destroy! + render json: { error: false, id: params[:id] } + end + + private + + def set_shortcut + @shortcut = Shortcut.where(account: current_account).find(params[:id]) + end + + def shortcut_params + params.permit(:shortcut_type, :shortcut_id) + end +end diff --git a/app/controllers/react_controller.rb b/app/controllers/react_controller.rb index e7370177..c5e0ffcb 100644 --- a/app/controllers/react_controller.rb +++ b/app/controllers/react_controller.rb @@ -36,7 +36,7 @@ class ReactController < ApplicationController end def find_route_matches - request.path.match(/\A\/(home|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|domain_blocks|mutes)/) + request.path.match(/\A\/(home|shortcuts|group|groups|list|lists|notifications|tags|compose|follow_requests|admin|account|settings|filters|timeline|blocks|domain_blocks|mutes)/) end def find_public_route_matches diff --git a/app/javascript/gabsocial/actions/shortcuts.js b/app/javascript/gabsocial/actions/shortcuts.js new file mode 100644 index 00000000..72a82bab --- /dev/null +++ b/app/javascript/gabsocial/actions/shortcuts.js @@ -0,0 +1,128 @@ +import { me } from '../initial_state' +import api from '../api' + +export const SHORTCUTS_FETCH_REQUEST = 'SHORTCUTS_FETCH_REQUEST' +export const SHORTCUTS_FETCH_SUCCESS = 'SHORTCUTS_FETCH_SUCCESS' +export const SHORTCUTS_FETCH_FAIL = 'SHORTCUTS_FETCH_FAIL' + +export const SHORTCUTS_ADD_REQUEST = 'SHORTCUTS_ADD_REQUEST' +export const SHORTCUTS_ADD_SUCCESS = 'SHORTCUTS_ADD_SUCCESS' +export const SHORTCUTS_ADD_FAIL = 'SHORTCUTS_ADD_FAIL' + +export const SHORTCUTS_REMOVE_REQUEST = 'SHORTCUTS_REMOVE_REQUEST' +export const SHORTCUTS_REMOVE_SUCCESS = 'SHORTCUTS_REMOVE_SUCCESS' +export const SHORTCUTS_REMOVE_FAIL = 'SHORTCUTS_REMOVE_FAIL' + +export function fetchShortcuts() { + return (dispatch, getState) => { + if (!me) return + + dispatch(fetchShortcutsRequest()) + + api(getState).get('/api/v1/shortcuts').then(response => { + dispatch(fetchShortcutsSuccess(response.data)) + }).catch(error => dispatch(fetchShortcutsFail(error))) + } +} + +export function fetchShortcutsRequest() { + return { + type: SHORTCUTS_FETCH_REQUEST, + } +} + +export function fetchShortcutsSuccess(shortcuts) { + return { + shortcuts, + type: SHORTCUTS_FETCH_SUCCESS, + } +} + +export function fetchShortcutsFail(error) { + return { + error, + type: SHORTCUTS_FETCH_FAIL, + } +} + +export function addShortcut(shortcutType, shortcutId) { + return (dispatch, getState) => { + if (!me) return + + dispatch(addShortcutsRequest()) + + api(getState).post('/api/v1/shortcuts', { + shortcut_type: shortcutType, + shortcut_id: shortcutId, + }).then(response => { + dispatch(addShortcutsSuccess(response.data)) + }).catch(error => dispatch(addShortcutsFail(error))) + } +} + +export function addShortcutsRequest() { + return { + type: SHORTCUTS_ADD_REQUEST, + } +} + +export function addShortcutsSuccess(shortcut) { + return { + shortcut, + type: SHORTCUTS_ADD_SUCCESS, + } +} + +export function addShortcutsFail(error) { + return { + error, + type: SHORTCUTS_ADD_FAIL, + } +} + +export function removeShortcut(shortcutObjectId, shortcutType, shortcutId) { + return (dispatch, getState) => { + if (!me) return + + let id + if (shortcutObjectId) { + shortcutObjectId = id + } else if (shortcutType && shortcutId) { + const shortcuts = getState().getIn(['shortcuts', 'items']) + const shortcut = shortcuts.find((s) => { + return s.get('shortcut_id') == shortcutId && s.get('shortcut_type') === shortcutType + }) + if (!!shortcut) { + id = shortcut.get('id') + } + } + + if (!id) return + + dispatch(removeShortcutsRequest()) + + api(getState).delete(`/api/v1/shortcuts/${id}`).then(response => { + dispatch(removeShortcutsSuccess(response.data.id)) + }).catch(error => dispatch(removeShortcutsFail(error))) + } +} + +export function removeShortcutsRequest() { + return { + type: SHORTCUTS_REMOVE_REQUEST, + } +} + +export function removeShortcutsSuccess(shortcutId) { + return { + shortcutId, + type: SHORTCUTS_REMOVE_SUCCESS, + } +} + +export function removeShortcutsFail(error) { + return { + error, + type: SHORTCUTS_REMOVE_FAIL, + } +} diff --git a/app/javascript/gabsocial/components/modal/edit_shortcuts_modal.js b/app/javascript/gabsocial/components/modal/edit_shortcuts_modal.js new file mode 100644 index 00000000..9572013b --- /dev/null +++ b/app/javascript/gabsocial/components/modal/edit_shortcuts_modal.js @@ -0,0 +1,71 @@ +import { defineMessages, injectIntl } from 'react-intl' +import ImmutablePureComponent from 'react-immutable-pure-component' +import ImmutablePropTypes from 'react-immutable-proptypes' +import { removeShortcut } from '../../actions/shortcuts' +import ModalLayout from './modal_layout' +import List from '../list' + +const messages = defineMessages({ + title: { id: 'shortcuts.edit', defaultMessage: 'Edit Shortcuts' }, + close: { id: 'lightbox.close', defaultMessage: 'Close' }, +}) + +const mapStateToProps = (state) => ({ + shortcuts: state.getIn(['shortcuts', 'items']), +}) + +const mapDispatchToProps = (dispatch) => ({ + onRemoveShortcut(shortcutId) { + dispatch(removeShortcut(shortcutId)) + }, +}) + +export default +@injectIntl +@connect(mapStateToProps, mapDispatchToProps) +class EditShortcutsModal extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + onClose: PropTypes.func.isRequired, + onRemoveShortcut: PropTypes.func.isRequired, + shortcuts: ImmutablePropTypes.list, + } + + handleOnRemoveShortcut = (shortcutId) => { + this.props.onRemoveShortcut(shortcutId) + } + + render() { + const { + intl, + onClose, + shortcuts, + } = this.props + + const listItems = shortcuts.map((s) => ({ + title: s.get('title'), + image: s.get('image'), + actionIcon: 'subtract', + onClick: () => this.handleOnRemoveShortcut(s.get('id')), + })) + + return ( + +
+ +
+
+ ) + } + +} diff --git a/app/javascript/gabsocial/components/popover/group_options_popover.js b/app/javascript/gabsocial/components/popover/group_options_popover.js index 9567bcce..646d1e83 100644 --- a/app/javascript/gabsocial/components/popover/group_options_popover.js +++ b/app/javascript/gabsocial/components/popover/group_options_popover.js @@ -6,9 +6,12 @@ import { MODAL_GROUP_MEMBERS, MODAL_GROUP_REMOVED_ACCOUNTS, } from '../../constants' +import { + addShortcut, + removeShortcut, +} from '../../actions/shortcuts' import { openModal } from '../../actions/modal' import { closePopover } from '../../actions/popover' -import { me } from '../../initial_state' import PopoverLayout from './popover_layout' import List from '../list' @@ -16,7 +19,18 @@ const messages = defineMessages({ groupMembers: { id: 'group_members', defaultMessage: 'Group members' }, removedMembers: { id: 'group_removed_members', defaultMessage: 'Removed accounts' }, editGroup: { id: 'edit_group', defaultMessage: 'Edit group' }, -}); + add_to_shortcuts: { id: 'account.add_to_shortcuts', defaultMessage: 'Add to shortcuts' }, + remove_from_shortcuts: { id: 'account.remove_from_shortcuts', defaultMessage: 'Remove from shortcuts' }, +}) + +const mapStateToProps = (state, { group }) => { + const groupId = group ? group.get('id') : null + const shortcuts = state.getIn(['shortcuts', 'items']) + const isShortcut = !!shortcuts.find((s) => { + return s.get('shortcut_id') == groupId && s.get('shortcut_type') === 'group' + }) + return { isShortcut } +} const mapDispatchToProps = (dispatch) => ({ @@ -24,38 +38,43 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(closePopover()) dispatch(openModal(MODAL_GROUP_CREATE, { groupId })) }, - onOpenRemovedMembers(groupId) { dispatch(closePopover()) dispatch(openModal(MODAL_GROUP_REMOVED_ACCOUNTS, { groupId })) }, - onOpenGroupMembers(groupId) { dispatch(closePopover()) dispatch(openModal(MODAL_GROUP_MEMBERS, { groupId })) }, + onClosePopover: () => dispatch(closePopover()), + onAddShortcut(groupId) { + dispatch(addShortcut('group', groupId)) + }, + onRemoveShortcut(groupId) { + dispatch(removeShortcut(null, 'group', groupId)) + }, - onClosePopover: () => dispatch(closePopover()) - -}); +}) export default @injectIntl -@connect(null, mapDispatchToProps) +@connect(mapStateToProps, mapDispatchToProps) class GroupOptionsPopover extends ImmutablePureComponent { static defaultProps = { group: ImmutablePropTypes.map.isRequired, + isAdmin: PropTypes.bool, intl: PropTypes.object.isRequired, isXS: PropTypes.bool, + isShortcut: PropTypes.bool, + onAddShortcut: PropTypes.func.isRequired, + onRemoveShortcut: PropTypes.func.isRequired, onClosePopover: PropTypes.func.isRequired, onOpenEditGroup: PropTypes.func.isRequired, onOpenGroupMembers: PropTypes.func.isRequired, onOpenRemovedMembers: PropTypes.func.isRequired, } - updateOnProps = ['group'] - handleEditGroup = () => { this.props.onOpenEditGroup(this.props.group.get('id')) } @@ -72,8 +91,22 @@ class GroupOptionsPopover extends ImmutablePureComponent { this.props.onClosePopover() } + handleOnToggleShortcut = () => { + this.handleOnClosePopover() + if (this.props.isShortcut) { + this.props.onRemoveShortcut(this.props.group.get('id')) + } else { + this.props.onAddShortcut(this.props.group.get('id')) + } + } + render() { - const { intl, isXS } = this.props + const { + intl, + isAdmin, + isShortcut, + isXS, + } = this.props const listItems = [ { @@ -81,24 +114,33 @@ class GroupOptionsPopover extends ImmutablePureComponent { icon: 'group', title: intl.formatMessage(messages.groupMembers), onClick: this.handleOnOpenGroupMembers, + isHidden: !isAdmin, }, { hideArrow: true, icon: 'block', title: intl.formatMessage(messages.removedMembers), onClick: this.handleOnOpenRemovedMembers, + isHidden: !isAdmin, }, { hideArrow: true, icon: 'pencil', title: intl.formatMessage(messages.editGroup), onClick: this.handleEditGroup, - } + isHidden: !isAdmin, + }, + { + hideArrow: true, + icon: 'star', + title: intl.formatMessage(isShortcut ? messages.remove_from_shortcuts : messages.add_to_shortcuts), + onClick: this.handleOnToggleShortcut, + }, ] return ( diff --git a/app/javascript/gabsocial/components/popover/profile_options_popover.js b/app/javascript/gabsocial/components/popover/profile_options_popover.js index f0ebab4f..d2fbd004 100644 --- a/app/javascript/gabsocial/components/popover/profile_options_popover.js +++ b/app/javascript/gabsocial/components/popover/profile_options_popover.js @@ -2,23 +2,22 @@ import { defineMessages, injectIntl } from 'react-intl' import { followAccount, unfollowAccount, - blockAccount, unblockAccount, unmuteAccount, - pinAccount, - unpinAccount, } from '../../actions/accounts' import { mentionCompose, } from '../../actions/compose' -import { muteAccount } from '../../actions/accounts' +import { + addShortcut, + removeShortcut, +} from '../../actions/shortcuts' import { initReport } from '../../actions/reports' import { openModal } from '../../actions/modal' import { closePopover } from '../../actions/popover' -import { unfollowModal, autoPlayGif, me, isStaff } from '../../initial_state' +import { unfollowModal, me, isStaff } from '../../initial_state' import { makeGetAccount } from '../../selectors' import PopoverLayout from './popover_layout' -import Text from '../text' import List from '../list' const messages = defineMessages({ @@ -44,23 +43,27 @@ const messages = defineMessages({ mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, admin_account: { id: 'admin_account', defaultMessage: 'Open moderation interface' }, add_to_list: { id: 'lists.account.add', defaultMessage: 'Add to list' }, - add_or_remove_from_shortcuts: { id: 'account.add_or_remove_from_shortcuts', defaultMessage: 'Add or Remove from shortcuts' }, + add_to_shortcuts: { id: 'account.add_to_shortcuts', defaultMessage: 'Add to shortcuts' }, + remove_from_shortcuts: { id: 'account.remove_from_shortcuts', defaultMessage: 'Remove from shortcuts' }, accountBlocked: { id: 'account.blocked', defaultMessage: 'Blocked' }, accountMuted: { id: 'account.muted', defaultMessage: 'Muted' }, }); -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); +const mapStateToProps = (state, { account }) => { + const getAccount = makeGetAccount() + const accountId = !!account ? account.get('id') : -1 + const shortcuts = state.getIn(['shortcuts', 'items']) + const isShortcut = !!shortcuts.find((s) => { + return s.get('shortcut_id') == accountId && s.get('shortcut_type') === 'account' + }) - const mapStateToProps = (state, { account }) => ({ - account: getAccount(state, !!account ? account.get('id') : -1), - }); - - return mapStateToProps; -}; + return { + isShortcut, + account: getAccount(state, accountId), + } +} const mapDispatchToProps = (dispatch, { intl }) => ({ - onFollow(account) { if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) { if (unfollowModal) { @@ -74,7 +77,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(followAccount(account.get('id'))) } }, - onBlock(account) { dispatch(closePopover()) @@ -86,12 +88,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); } }, - onMention(account) { dispatch(closePopover()) dispatch(mentionCompose(account)); }, - onRepostToggle(account) { dispatch(closePopover()) if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -100,12 +100,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(followAccount(account.get('id'), true)); } }, - onReport(account) { dispatch(closePopover()) dispatch(initReport(account)); }, - onMute(account) { dispatch(closePopover()) if (account.getIn(['relationship', 'muting'])) { @@ -116,30 +114,39 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })) } }, - onAddToList(account) { dispatch(closePopover()) dispatch(openModal('LIST_ADD_USER', { accountId: account.get('id'), })); }, - onClosePopover: () => dispatch(closePopover()), - -}); - + onAddShortcut(accountId) { + dispatch(closePopover()) + dispatch(addShortcut('account', accountId)) + }, + onRemoveShortcut(accountId) { + dispatch(closePopover()) + dispatch(removeShortcut(null, 'account', accountId)) + }, +}) export default @injectIntl -@connect(makeMapStateToProps, mapDispatchToProps) +@connect(mapStateToProps, mapDispatchToProps) class ProfileOptionsPopover extends PureComponent { static defaultProps = { isXS: PropTypes.bool, + isShortcut: PropTypes.bool, } makeMenu() { - const { account, intl } = this.props; + const { + account, + intl, + isShortcut, + } = this.props; let menu = []; @@ -208,12 +215,12 @@ class ProfileOptionsPopover extends PureComponent { // onClick: this.handleAddToList // }) - // menu.push({ - // hideArrow: true, - // icon: 'circle', - // title: intl.formatMessage(messages.add_or_remove_from_shortcuts), - // onClick: this.handleAddToShortcuts - // }) + menu.push({ + hideArrow: true, + icon: 'star', + title: intl.formatMessage(isShortcut ? messages.remove_from_shortcuts : messages.add_to_shortcuts), + onClick: this.handleToggleShortcuts, + }) if (isStaff) { menu.push({ @@ -259,8 +266,12 @@ class ProfileOptionsPopover extends PureComponent { this.props.onAddToList(this.props.account); } - handleAddToShortcuts = () => { - // : todo : + handleToggleShortcuts = () => { + if (this.props.isShortcut) { + this.props.onRemoveShortcut(this.props.account.get('id')) + } else { + this.props.onAddShortcut(this.props.account.get('id')) + } } handleOnClosePopover = () => { diff --git a/app/javascript/gabsocial/components/sidebar.js b/app/javascript/gabsocial/components/sidebar.js index 761ade7f..6ff81608 100644 --- a/app/javascript/gabsocial/components/sidebar.js +++ b/app/javascript/gabsocial/components/sidebar.js @@ -1,11 +1,15 @@ +import { Fragment } from 'react' import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePureComponent from 'react-immutable-pure-component' import { injectIntl, defineMessages } from 'react-intl' -import * as Constants from '../constants' +import { + BREAKPOINT_SMALL, +} from '../constants' import Button from './button' import { closeSidebar } from '../actions/sidebar' import { openModal } from '../actions/modal' import { openPopover } from '../actions/popover' +import { fetchShortcuts } from '../actions/shortcuts' import { me } from '../initial_state' import { makeGetAccount } from '../selectors' import Responsive from '../features/ui/util/responsive_component' @@ -14,6 +18,7 @@ import SidebarSectionItem from './sidebar_section_item' import Heading from './heading' import BackButton from './back_button' import Pills from './pills' +import Text from './text' const messages = defineMessages({ followers: { id: 'account.followers', defaultMessage: 'Followers' }, @@ -35,10 +40,14 @@ const messages = defineMessages({ search: { id: 'tabs_bar.search', defaultMessage: 'Search' }, shop: { id: 'tabs_bar.shop', defaultMessage: 'Store - Buy Merch' }, donate: { id: 'tabs_bar.donate', defaultMessage: 'Make a Donation' }, + shortcuts: { id: 'navigation_bar.shortcuts', defaultMessage: 'Shortcuts' }, + all: { id: 'all', defaultMessage: 'All' }, + edit: { id: 'edit', defaultMessage: 'Edit' }, }) const mapStateToProps = (state) => ({ account: makeGetAccount()(state, me), + shortcuts: state.getIn(['shortcuts', 'items']), moreOpen: state.getIn(['popover', 'popoverType']) === 'SIDEBAR_MORE', notificationCount: state.getIn(['notifications', 'unread']), homeItemsQueueCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']), @@ -54,6 +63,9 @@ const mapDispatchToProps = (dispatch) => ({ onOpenComposeModal() { dispatch(openModal('COMPOSE')) }, + onFetchShortcuts() { + dispatch(fetchShortcuts()) + }, }) export default @@ -67,6 +79,7 @@ class Sidebar extends ImmutablePureComponent { moreOpen: PropTypes.bool, onClose: PropTypes.func.isRequired, onOpenComposeModal: PropTypes.func.isRequired, + onFetchShortcuts: PropTypes.func.isRequired, openSidebarMorePopover: PropTypes.func.isRequired, notificationCount: PropTypes.number.isRequired, homeItemsQueueCount: PropTypes.number.isRequired, @@ -74,6 +87,15 @@ class Sidebar extends ImmutablePureComponent { tabs: PropTypes.array, title: PropTypes.string, showBackBtn: PropTypes.bool, + shortcuts: ImmutablePropTypes.list, + } + + state = { + hoveringShortcuts: false, + } + + componentDidMount() { + this.props.onFetchShortcuts() } handleOpenComposeModal = () => { @@ -87,6 +109,14 @@ class Sidebar extends ImmutablePureComponent { }) } + handleMouseEnterShortcuts = () => { + this.setState({ hoveringShortcuts: true }) + } + + handleMouseLeaveShortcuts = () => { + this.setState({ hoveringShortcuts: false }) + } + setMoreButtonRef = n => { this.moreBtnRef = n } @@ -102,14 +132,12 @@ class Sidebar extends ImmutablePureComponent { tabs, title, showBackBtn, + shortcuts, } = this.props + const { hoveringShortcuts } = this.state - // : todo : if (!me || !account) return null - const acct = account.get('acct') - const isPro = account.get('is_pro') - const menuItems = [ { title: 'Home', @@ -144,6 +172,11 @@ class Sidebar extends ImmutablePureComponent { icon: 'explore', to: '/explore', }, + { + title: 'Pro Feed', + icon: 'circle', + to: '/timeline/pro', + }, { title: 'More', icon: 'more', @@ -153,32 +186,18 @@ class Sidebar extends ImmutablePureComponent { }, ] - const shortcutItems = [ - // { - // title: 'Meme Group', - // icon: 'group', - // to: '/', - // count: 0, - // }, - // { - // title: '@andrew', - // image: 'http://localhost:3000/system/accounts/avatars/000/000/001/original/260e8c96c97834da.jpeg?1562898139', - // to: '/', - // count: 3, - // }, - ] + let shortcutItems = [] + if (!!shortcuts) { + shortcuts.forEach((s) => { + shortcutItems.push({ + to: s.get('to'), + title: s.get('title'), + image: s.get('image'), + }) + }) + } const exploreItems = [ - { - title: 'Pro Feed', - icon: 'circle', - to: '/timeline/pro', - }, - { - title: 'Chat', - icon: 'chat', - href: 'https://chat.gab.com', - }, { title: 'Apps', icon: 'apps', @@ -259,6 +278,41 @@ class Sidebar extends ImmutablePureComponent { ) }) } + { + !!shortcutItems.length > 0 && + + +
+ + {intl.formatMessage(messages.shortcuts)} + + +
+
+ { + shortcutItems.map((shortcutItem, i) => ( + + )) + } +
+ } {intl.formatMessage(messages.explore)} { exploreItems.map((exploreItem, i) => ( @@ -267,7 +321,7 @@ class Sidebar extends ImmutablePureComponent { } - +