Added shortcuts

• Added:
- shortcuts functionality
- shortcuts route, controller, model
- shortcut error message for "exists"
- shortcut redux
- EditShortcutsModal, constant
- links to sidebar, sidebar_xs
- options to add/remove group, account in GroupOptionsPopover, ProfileOptionsPopover
- shortcuts page, feature/list
This commit is contained in:
mgabdev 2020-07-21 22:24:26 -05:00
parent 405ace09da
commit f92f75d747
20 changed files with 705 additions and 107 deletions

@ -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

@ -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

@ -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,
}
}

@ -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 (
<ModalLayout
title={intl.formatMessage(messages.title)}
onClose={onClose}
width={460}
noPadding
>
<div className={_s.boxShadowNone}>
<List
scrollKey='shortcuts'
emptyMessage='You have no shortcuts'
items={listItems}
/>
</div>
</ModalLayout>
)
}
}

@ -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 (
<PopoverLayout
width={210}
width={240}
isXS={isXS}
onClose={this.handleOnClosePopover}
>

@ -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 = () => {

@ -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 &&
<Fragment>
<SidebarSectionTitle>
<div
className={[_s.displayFlex, _s.alignItemsCenter, _s.flexRow].join(' ')}
onMouseEnter={this.handleMouseEnterShortcuts}
onMouseLeave={this.handleMouseLeaveShortcuts}
>
<span>
{intl.formatMessage(messages.shortcuts)}
</span>
<Button
isText
to='/shortcuts'
color='brand'
backgroundColor='none'
className={_s.mlAuto}
>
{
hoveringShortcuts &&
<Text color='inherit' size='small' weight='medium' align='right'>
{intl.formatMessage(messages.all)}
</Text>
}
</Button>
</div>
</SidebarSectionTitle>
{
shortcutItems.map((shortcutItem, i) => (
<SidebarSectionItem {...shortcutItem} key={`sidebar-item-shortcut-${i}`} />
))
}
</Fragment>
}
<SidebarSectionTitle>{intl.formatMessage(messages.explore)}</SidebarSectionTitle>
{
exploreItems.map((exploreItem, i) => (
@ -267,7 +321,7 @@ class Sidebar extends ImmutablePureComponent {
}
</nav>
<Responsive min={Constants.BREAKPOINT_SMALL}>
<Responsive min={BREAKPOINT_SMALL}>
<Button
isBlock
onClick={this.handleOpenComposeModal}
@ -277,7 +331,7 @@ class Sidebar extends ImmutablePureComponent {
</Button>
</Responsive>
<Responsive max={Constants.BREAKPOINT_SMALL}>
<Responsive max={BREAKPOINT_SMALL}>
<Button
onClick={this.handleOpenComposeModal}
className={_s.py15}

@ -40,6 +40,7 @@ const messages = defineMessages({
help: { id: 'getting_started.help', defaultMessage: 'Help' },
display: { id: 'display_options', defaultMessage: 'Display Options' },
proFeed: { id: 'pro_feed', defaultMessage: 'Pro Feed' },
shortcuts: { id: 'shortcuts', defaultMessage: 'Shortcuts' },
})
const mapStateToProps = (state) => ({
@ -101,6 +102,12 @@ class SidebarXS extends ImmutablePureComponent {
onClick: this.handleSidebarClose,
title: intl.formatMessage(messages.lists),
},
{
icon: 'star',
to: '/shortcuts',
onClick: this.handleSidebarClose,
title: intl.formatMessage(messages.shortcuts),
},
{
icon: 'pro',
href: 'https://pro.gab.com',

@ -41,6 +41,7 @@ export const MODAL_COMPOSE = 'COMPOSE'
export const MODAL_CONFIRM = 'CONFIRM'
export const MODAL_DISPLAY_OPTIONS = 'DISPLAY_OPTIONS'
export const MODAL_EDIT_PROFILE = 'EDIT_PROFILE'
export const MODAL_EDIT_SHORTCUTS = 'EDIT_SHORTCUTS'
export const MODAL_EMBED = 'EMBED'
export const MODAL_GIF_PICKER = 'GIF_PICKER'
export const MODAL_GROUP_CREATE = 'GROUP_CREATE'

@ -0,0 +1,62 @@
import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes'
import { fetchShortcuts } from '../actions/shortcuts'
import ColumnIndicator from '../components/column_indicator'
import List from '../components/list'
const mapStateToProps = (state) => ({
isError: state.getIn(['shortcuts', 'isError']),
isLoading: state.getIn(['shortcuts', 'isLoading']),
shortcuts: state.getIn(['shortcuts', 'items']),
})
const mapDispatchToProps = (dispatch) => ({
onFetchShortcuts() {
dispatch(fetchShortcuts())
},
})
export default
@connect(mapStateToProps, mapDispatchToProps)
class Shortcuts extends ImmutablePureComponent {
static propTypes = {
isLoading: PropTypes.bool.isRequired,
isError: PropTypes.bool.isRequired,
onFetchShortcuts: PropTypes.func.isRequired,
shortcuts: ImmutablePropTypes.list,
}
componentDidMount() {
this.props.onFetchShortcuts()
}
render() {
const {
isLoading,
isError,
shortcuts,
} = this.props
if (isLoading) {
return <ColumnIndicator type='loading' />
} else if (isError) {
return <ColumnIndicator type='error' message='Error fetching shortcuts' />
}
const listItems = shortcuts.map((s) => ({
to: s.get('to'),
title: s.get('title'),
image: s.get('image'),
}))
return (
<List
scrollKey='shortcuts'
emptyMessage='You have no shortcuts'
items={listItems}
/>
)
}
}

@ -75,7 +75,7 @@ import {
PrivacyPolicy,
ProTimeline,
Search,
// Shortcuts,
Shortcuts,
StatusFeature,
StatusLikes,
StatusReposts,
@ -188,7 +188,7 @@ class SwitchingArea extends PureComponent {
<WrappedRoute path='/tags/:id' publicRoute page={HashtagPage} component={HashtagTimeline} content={children} componentParams={{ title: 'Hashtag' }} />
{ /* <WrappedRoute path='/shortcuts' publicRoute page={ShortcutsPage} component={Shortcuts} content={children} /> */ }
<WrappedRoute path='/shortcuts' page={ShortcutsPage} component={Shortcuts} content={children} />
<WrappedRoute path='/lists' exact page={ListsPage} component={ListsDirectory} content={children} />
<WrappedRoute path='/lists/create' exact page={ModalPage} component={ListCreate} content={children} componentParams={{ title: 'Create List', page: 'create-list' }} />

@ -15,6 +15,7 @@ export function DatePickerPopover() { return import(/* webpackChunkName: "compon
export function DisplayOptionsModal() { return import(/* webpackChunkName: "components/display_options_modal" */'../../../components/modal/display_options_modal') }
export function DMCA() { return import(/* webpackChunkName: "features/about/dmca" */'../../about/dmca') }
export function EditProfileModal() { return import(/* webpackChunkName: "components/edit_profile_modal" */'../../../components/modal/edit_profile_modal') }
export function EditShortcutsModal() { return import(/* webpackChunkName: "components/edit_shortcuts_modal" */'../../../components/modal/edit_shortcuts_modal') }
export function EmbedModal() { return import(/* webpackChunkName: "modals/embed_modal" */'../../../components/modal/embed_modal') }
export function EmojiPicker() { return import(/* webpackChunkName: "emoji_picker" */'../../../components/emoji/emoji_picker') }
export function EmojiPickerPopover() { return import(/* webpackChunkName: "components/emoji_picker_popover" */'../../../components/popover/emoji_picker_popover') }
@ -64,6 +65,7 @@ export function ProfileOptionsPopover() { return import(/* webpackChunkName: "co
export function ProUpgradeModal() { return import(/* webpackChunkName: "components/pro_upgrade_modal" */'../../../components/modal/pro_upgrade_modal') }
export function ReportModal() { return import(/* webpackChunkName: "modals/report_modal" */'../../../components/modal/report_modal') }
export function Search() { return import(/*webpackChunkName: "features/search" */'../../search') }
export function Shortcuts() { return import(/*webpackChunkName: "features/shortcuts" */'../../shortcuts') }
export function Status() { return import(/* webpackChunkName: "components/status" */'../../../components/status') }
export function StatusFeature() { return import(/* webpackChunkName: "features/status" */'../../status') }
export function SearchPopover() { return import(/* webpackChunkName: "components/search_popover" */'../../../components/popover/search_popover') }

@ -1,34 +1,58 @@
import { Fragment } from 'react'
import { defineMessages, injectIntl } from 'react-intl'
import { openModal } from '../actions/modal'
import GroupSidebarPanel from '../components/panel/groups_panel'
import { MODAL_EDIT_SHORTCUTS } from '../constants'
import PageTitle from '../features/ui/util/page_title'
import LinkFooter from '../components/link_footer'
import WhoToFollowPanel from '../components/panel/who_to_follow_panel'
import ProgressPanel from '../components/panel/progress_panel'
import UserPanel from '../components/panel/user_panel'
import TrendsPanel from '../components/panel/trends_panel'
import DefaultLayout from '../layouts/default_layout'
const messages = defineMessages({
shortcuts: { id: 'shortcuts', defaultMessage: 'Shortcuts' },
})
const mapDispatchToProps = (dispatch) => ({
onOpenEditShortcutsModal() {
dispatch(openModal(MODAL_EDIT_SHORTCUTS))
},
})
export default
@injectIntl
@connect(null, mapDispatchToProps)
class ShortcutsPage extends PureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
onOpenEditShortcutsModal: PropTypes.func.isRequired,
}
handleOnOpenEditShortcutsModal = () => {
this.props.onOpenEditShortcutsModal()
}
render() {
const { children } = this.props
const { intl, children } = this.props
const title = intl.formatMessage(messages.shortcuts)
return (
<DefaultLayout
title='Shortcuts'
actions={[]}
layout={(
<Fragment>
<UserPanel />
<ProgressPanel />
<TrendsPanel />
<WhoToFollowPanel />
<GroupSidebarPanel />
<LinkFooter />
</Fragment>
)}
title={title}
page='shortcuts'
actions={[
{
icon: 'cog',
onClick: this.handleOnOpenEditShortcutsModal,
},
]}
layout={[
<TrendsPanel key='shortcuts-page-trends-panel' />,
<WhoToFollowPanel key='shortcuts-page-wtf-panel' />,
<LinkFooter key='shortcuts-page-link-footer' />,
]}
>
<PageTitle path={title} />
{children}
</DefaultLayout>
)

@ -30,6 +30,7 @@ import reports from './reports'
import search from './search'
import settings from './settings'
import shop from './shop'
import shortcuts from './shortcuts'
import sidebar from './sidebar'
import statuses from './statuses'
import status_lists from './status_lists'
@ -72,6 +73,7 @@ const reducers = {
search,
settings,
shop,
shortcuts,
sidebar,
statuses,
status_lists,

@ -0,0 +1,75 @@
import {
SHORTCUTS_FETCH_REQUEST,
SHORTCUTS_FETCH_SUCCESS,
SHORTCUTS_FETCH_FAIL,
SHORTCUTS_ADD_SUCCESS,
SHORTCUTS_REMOVE_SUCCESS,
} from '../actions/shortcuts'
import { importFetchedAccount } from '../actions/importer'
import { importGroup } from '../actions/groups'
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'
const initialState = ImmutableMap({
items: ImmutableList(),
isLoading: false,
isError: false,
})
const normalizeShortcut = (shortcut) => {
if (shortcut.shortcut_type === 'account') {
importFetchedAccount(shortcut.shortcut)
return {
id: shortcut.id,
shortcut_type: 'account',
shortcut_id: shortcut.shortcut_id,
title: shortcut.shortcut.acct,
image: shortcut.shortcut.avatar_static,
to: `/${shortcut.shortcut.acct}`,
}
} else if (shortcut.shortcut_type === 'group') {
importGroup(shortcut.shortcut)
return {
id: shortcut.id,
shortcut_type: 'group',
shortcut_id: shortcut.shortcut_id,
title: shortcut.shortcut.title,
image: shortcut.shortcut.cover_image_url,
to: `/groups/${shortcut.shortcut.id}`,
}
}
}
const normalizeShortcuts = (shortcuts) => {
return fromJS(shortcuts.map((shortcut) => {
return normalizeShortcut(shortcut)
}))
}
export default function shortcutsReducer(state = initialState, action) {
switch(action.type) {
case SHORTCUTS_FETCH_REQUEST:
return state.withMutations((map) => {
map.set('isLoading', true)
map.set('isError', false)
})
case SHORTCUTS_FETCH_SUCCESS:
return state.withMutations((map) => {
map.set('items', normalizeShortcuts(action.shortcuts))
map.set('isLoading', false)
map.set('isError', false)
})
case SHORTCUTS_FETCH_FAIL:
return state.withMutations((map) => {
map.set('isLoading', false)
map.set('isError', true)
})
case SHORTCUTS_ADD_SUCCESS:
return state.update('items', list => list.push(fromJS(normalizeShortcut(action.shortcut))))
case SHORTCUTS_REMOVE_SUCCESS:
return state.update('items', list => list.filterNot((item) => {
return `${item.get('id')}` === `${action.shortcutId}`
}))
default:
return state
}
}

@ -3,14 +3,25 @@
#
# Table name: shortcuts
#
# id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# shortcut_id :bigint(8) not null
# shortcut_type :string not null
# created_at :datetime not null
# updated_at :datetime
# id :bigint(8) not null, primary key
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint(8) not null
# shortcut_id :bigint(8) not null
# shortcut_type :string default(""), not null
#
class Shortcut < ApplicationRecord
# enum shortcut_type: {
# account: 'account',
# group: 'group'
# }
belongs_to :account
PER_ACCOUNT_LIMIT = 50
validates_each :account_id, on: :create do |record, _attr, value|
record.errors.add(:base, I18n.t('shortcuts.errors.limit')) if Shortcut.where(account_id: value).count >= PER_ACCOUNT_LIMIT
end
end

@ -0,0 +1,10 @@
# frozen_string_literal: true
class REST::ShortcutSerializer < ActiveModel::Serializer
attributes :id, :account_id, :created_at, :shortcut_type, :shortcut_id
def id
object.id.to_s
end
end

@ -837,6 +837,7 @@ en:
shortcuts:
errors:
limit: You have reached the maximum amount of shortcuts
exists: You already have a shortcut for that
sessions:
activity: Last activity
browser: Browser

@ -795,6 +795,7 @@ en_GB:
shortcuts:
errors:
limit: You have reached the maximum amount of shortcuts
exists: You already have a shortcut for that
sessions:
activity: Last activity
browser: Browser

@ -359,6 +359,7 @@ Rails.application.routes.draw do
resources :reports, only: [:create]
resources :filters, only: [:index, :create, :show, :update, :destroy]
resources :endorsements, only: [:index]
resources :shortcuts, only: [:index, :create, :show, :destroy]
namespace :apps do
get :verify_credentials, to: 'credentials#show'