diff options
19 files changed, 351 insertions, 60 deletions
diff --git a/app/controllers/settings/migrations_controller.rb b/app/controllers/settings/migrations_controller.rb index bc6436b87..89b3f7246 100644 --- a/app/controllers/settings/migrations_controller.rb +++ b/app/controllers/settings/migrations_controller.rb @@ -1,10 +1,6 @@ # frozen_string_literal: true -class Settings::MigrationsController < ApplicationController - layout 'admin' - - before_action :authenticate_user! - +class Settings::MigrationsController < Settings::BaseController def show @migration = Form::Migration.new(account: current_account.moved_to_account) end diff --git a/app/javascript/flavours/glitch/actions/domain_blocks.js b/app/javascript/flavours/glitch/actions/domain_blocks.js index 8506df91c..46f76710c 100644 --- a/app/javascript/flavours/glitch/actions/domain_blocks.js +++ b/app/javascript/flavours/glitch/actions/domain_blocks.js @@ -12,12 +12,18 @@ export const DOMAIN_BLOCKS_FETCH_REQUEST = 'DOMAIN_BLOCKS_FETCH_REQUEST'; export const DOMAIN_BLOCKS_FETCH_SUCCESS = 'DOMAIN_BLOCKS_FETCH_SUCCESS'; export const DOMAIN_BLOCKS_FETCH_FAIL = 'DOMAIN_BLOCKS_FETCH_FAIL'; -export function blockDomain(domain, accountId) { +export const DOMAIN_BLOCKS_EXPAND_REQUEST = 'DOMAIN_BLOCKS_EXPAND_REQUEST'; +export const DOMAIN_BLOCKS_EXPAND_SUCCESS = 'DOMAIN_BLOCKS_EXPAND_SUCCESS'; +export const DOMAIN_BLOCKS_EXPAND_FAIL = 'DOMAIN_BLOCKS_EXPAND_FAIL'; + +export function blockDomain(domain) { return (dispatch, getState) => { dispatch(blockDomainRequest(domain)); api(getState).post('/api/v1/domain_blocks', { domain }).then(() => { - dispatch(blockDomainSuccess(domain, accountId)); + const at_domain = '@' + domain; + const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(blockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(blockDomainFail(domain, err)); }); @@ -31,11 +37,11 @@ export function blockDomainRequest(domain) { }; }; -export function blockDomainSuccess(domain, accountId) { +export function blockDomainSuccess(domain, accounts) { return { type: DOMAIN_BLOCK_SUCCESS, domain, - accountId, + accounts, }; }; @@ -47,12 +53,14 @@ export function blockDomainFail(domain, error) { }; }; -export function unblockDomain(domain, accountId) { +export function unblockDomain(domain) { return (dispatch, getState) => { dispatch(unblockDomainRequest(domain)); api(getState).delete('/api/v1/domain_blocks', { params: { domain } }).then(() => { - dispatch(unblockDomainSuccess(domain, accountId)); + const at_domain = '@' + domain; + const accounts = getState().get('accounts').filter(item => item.get('acct').endsWith(at_domain)).valueSeq().map(item => item.get('id')); + dispatch(unblockDomainSuccess(domain, accounts)); }).catch(err => { dispatch(unblockDomainFail(domain, err)); }); @@ -66,11 +74,11 @@ export function unblockDomainRequest(domain) { }; }; -export function unblockDomainSuccess(domain, accountId) { +export function unblockDomainSuccess(domain, accounts) { return { type: DOMAIN_UNBLOCK_SUCCESS, domain, - accountId, + accounts, }; }; @@ -86,7 +94,7 @@ export function fetchDomainBlocks() { return (dispatch, getState) => { dispatch(fetchDomainBlocksRequest()); - api(getState).get().then(response => { + api(getState).get('/api/v1/domain_blocks').then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); dispatch(fetchDomainBlocksSuccess(response.data, next ? next.uri : null)); }).catch(err => { @@ -115,3 +123,43 @@ export function fetchDomainBlocksFail(error) { error, }; }; + +export function expandDomainBlocks() { + return (dispatch, getState) => { + const url = getState().getIn(['domain_lists', 'blocks', 'next']); + + if (url === null) { + return; + } + + dispatch(expandDomainBlocksRequest()); + + api(getState).get(url).then(response => { + const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(expandDomainBlocksSuccess(response.data, next ? next.uri : null)); + }).catch(err => { + dispatch(expandDomainBlocksFail(err)); + }); + }; +}; + +export function expandDomainBlocksRequest() { + return { + type: DOMAIN_BLOCKS_EXPAND_REQUEST, + }; +}; + +export function expandDomainBlocksSuccess(domains, next) { + return { + type: DOMAIN_BLOCKS_EXPAND_SUCCESS, + domains, + next, + }; +}; + +export function expandDomainBlocksFail(error) { + return { + type: DOMAIN_BLOCKS_EXPAND_FAIL, + error, + }; +}; diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js new file mode 100644 index 000000000..217ba4e74 --- /dev/null +++ b/app/javascript/flavours/glitch/actions/dropdown_menu.js @@ -0,0 +1,10 @@ +export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; +export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; + +export function openDropdownMenu(id, placement) { + return { type: DROPDOWN_MENU_OPEN, id, placement }; +} + +export function closeDropdownMenu(id) { + return { type: DROPDOWN_MENU_CLOSE, id }; +} diff --git a/app/javascript/flavours/glitch/components/domain.js b/app/javascript/flavours/glitch/components/domain.js new file mode 100644 index 000000000..f657cb8d2 --- /dev/null +++ b/app/javascript/flavours/glitch/components/domain.js @@ -0,0 +1,42 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import IconButton from './icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +const messages = defineMessages({ + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, +}); + +@injectIntl +export default class Account extends ImmutablePureComponent { + + static propTypes = { + domain: PropTypes.string, + onUnblockDomain: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleDomainUnblock = () => { + this.props.onUnblockDomain(this.props.domain); + } + + render () { + const { domain, intl } = this.props; + + return ( + <div className='domain'> + <div className='domain__wrapper'> + <span className='domain__domain-name'> + <strong>{domain}</strong> + </span> + + <div className='domain__buttons'> + <IconButton active icon='unlock-alt' title={intl.formatMessage(messages.unblockDomain, { domain })} onClick={this.handleDomainUnblock} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 7ba7fb22b..245bebef3 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -8,6 +8,7 @@ import spring from 'react-motion/lib/spring'; import detectPassiveEvents from 'detect-passive-events'; const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; +let id = 0; class DropdownMenu extends React.PureComponent { @@ -29,6 +30,10 @@ class DropdownMenu extends React.PureComponent { placement: 'bottom', }; + state = { + mounted: false, + }; + handleDocumentClick = e => { if (this.node && !this.node.contains(e.target)) { this.props.onClose(); @@ -38,6 +43,7 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + this.setState({ mounted: true }); } componentWillUnmount () { @@ -82,11 +88,15 @@ class DropdownMenu extends React.PureComponent { render () { const { items, style, placement, arrowOffsetLeft, arrowOffsetTop } = this.props; + const { mounted } = this.state; return ( <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> {({ opacity, scaleX, scaleY }) => ( - <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }} ref={this.setRef}> + // It should not be transformed when mounting because the resulting + // size will be used to determine the coordinate of the menu by + // react-overlays + <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> <ul> @@ -115,8 +125,10 @@ export default class Dropdown extends React.PureComponent { status: ImmutablePropTypes.map, isUserTouching: PropTypes.func, isModalOpen: PropTypes.bool.isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, + onOpen: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + dropdownPlacement: PropTypes.string, + openDropdownId: PropTypes.number, }; static defaultProps = { @@ -124,42 +136,28 @@ export default class Dropdown extends React.PureComponent { }; state = { - expanded: false, + id: id++, }; - handleClick = () => { - if (!this.state.expanded && this.props.isUserTouching() && this.props.onModalOpen) { - const { status, items } = this.props; - - this.props.onModalOpen({ - status, - actions: items.map( - (item, i) => item ? { - ...item, - name: `${item.text}-${i}`, - onClick: this.handleItemClick.bind(this, i), - } : null - ), - }); - - return; - } + handleClick = ({ target }) => { + if (this.state.id === this.props.openDropdownId) { + this.handleClose(); + } else { + const { top } = target.getBoundingClientRect(); + const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.setState({ expanded: !this.state.expanded }); + this.props.onOpen(this.state.id, this.handleItemClick, placement); + } } handleClose = () => { - if (this.props.onModalClose) { - this.props.onModalClose(); - } - - this.setState({ expanded: false }); + this.props.onClose(this.state.id); } handleKeyDown = e => { switch(e.key) { case 'Enter': - this.handleClick(); + this.handleClick(e); break; case 'Escape': this.handleClose(); @@ -190,22 +188,22 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled } = this.props; - const { expanded } = this.state; + const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId } = this.props; + const open = this.state.id === openDropdownId; return ( <div onKeyDown={this.handleKeyDown}> <IconButton icon={icon} title={ariaLabel} - active={expanded} + active={open} disabled={disabled} size={size} ref={this.setTargetRef} onClick={this.handleClick} /> - <Overlay show={expanded} placement='bottom' target={this.findTarget}> + <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> <DropdownMenu items={items} onClose={this.handleClose} /> </Overlay> </div> diff --git a/app/javascript/flavours/glitch/containers/domain_container.js b/app/javascript/flavours/glitch/containers/domain_container.js new file mode 100644 index 000000000..52d5c1613 --- /dev/null +++ b/app/javascript/flavours/glitch/containers/domain_container.js @@ -0,0 +1,33 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { blockDomain, unblockDomain } from '../actions/domain_blocks'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import Domain from '../components/domain'; +import { openModal } from '../actions/modal'; + +const messages = defineMessages({ + blockDomainConfirm: { id: 'confirmations.domain_block.confirm', defaultMessage: 'Hide entire domain' }, +}); + +const makeMapStateToProps = () => { + const mapStateToProps = (state, { }) => ({ + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onBlockDomain (domain) { + dispatch(openModal('CONFIRM', { + message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, + confirm: intl.formatMessage(messages.blockDomainConfirm), + onConfirm: () => dispatch(blockDomain(domain)), + })); + }, + + onUnblockDomain (domain) { + dispatch(unblockDomain(domain)); + }, +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Domain)); diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index 0b4f72fa1..9d490de17 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -1,3 +1,4 @@ +import { openDropdownMenu, closeDropdownMenu } from 'flavours/glitch/actions/dropdown_menu'; import { openModal, closeModal } from 'flavours/glitch/actions/modal'; import { connect } from 'react-redux'; import DropdownMenu from 'flavours/glitch/components/dropdown_menu'; @@ -5,12 +6,22 @@ import { isUserTouching } from 'flavours/glitch/util/is_mobile'; const mapStateToProps = state => ({ isModalOpen: state.get('modal').modalType === 'ACTIONS', + dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), + openDropdownId: state.getIn(['dropdown_menu', 'openId']), }); -const mapDispatchToProps = dispatch => ({ - isUserTouching, - onModalOpen: props => dispatch(openModal('ACTIONS', props)), - onModalClose: () => dispatch(closeModal()), +const mapDispatchToProps = (dispatch, { status, items }) => ({ + onOpen(id, onItemClick, dropdownPlacement) { + dispatch(isUserTouching() ? openModal('ACTIONS', { + status, + actions: items, + onClick: onItemClick, + }) : openDropdownMenu(id, dropdownPlacement)); + }, + onClose(id) { + dispatch(closeModal()); + dispatch(closeDropdownMenu(id)); + }, }); export default connect(mapStateToProps, mapDispatchToProps)(DropdownMenu); diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index e788ea660..39a1850d7 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -57,7 +57,7 @@ export default class Header extends ImmutablePureComponent { if (!domain) return; - this.props.onBlockDomain(domain, this.props.account.get('id')); + this.props.onBlockDomain(domain); } handleUnblockDomain = () => { @@ -65,7 +65,7 @@ export default class Header extends ImmutablePureComponent { if (!domain) return; - this.props.onUnblockDomain(domain, this.props.account.get('id')); + this.props.onUnblockDomain(domain); } render () { diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index 37ff445b2..848119c63 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -87,16 +87,16 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, - onBlockDomain (domain, accountId) { + onBlockDomain (domain) { dispatch(openModal('CONFIRM', { message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable.' values={{ domain: <strong>{domain}</strong> }} />, confirm: intl.formatMessage(messages.blockDomainConfirm), - onConfirm: () => dispatch(blockDomain(domain, accountId)), + onConfirm: () => dispatch(blockDomain(domain)), })); }, - onUnblockDomain (domain, accountId) { - dispatch(unblockDomain(domain, accountId)); + onUnblockDomain (domain) { + dispatch(unblockDomain(domain)); }, }); diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js new file mode 100644 index 000000000..b17c47e91 --- /dev/null +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -0,0 +1,66 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import LoadingIndicator from '../../components/loading_indicator'; +import Column from '../ui/components/column'; +import ColumnBackButtonSlim from '../../components/column_back_button_slim'; +import DomainContainer from '../../containers/domain_container'; +import { fetchDomainBlocks, expandDomainBlocks } from '../../actions/domain_blocks'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { debounce } from 'lodash'; +import ScrollableList from '../../components/scrollable_list'; + +const messages = defineMessages({ + heading: { id: 'column.domain_blocks', defaultMessage: 'Hidden domains' }, + unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' }, +}); + +const mapStateToProps = state => ({ + domains: state.getIn(['domain_lists', 'blocks', 'items']), +}); + +@connect(mapStateToProps) +@injectIntl +export default class Blocks extends ImmutablePureComponent { + + static propTypes = { + params: PropTypes.object.isRequired, + dispatch: PropTypes.func.isRequired, + domains: ImmutablePropTypes.list, + intl: PropTypes.object.isRequired, + }; + + componentWillMount () { + this.props.dispatch(fetchDomainBlocks()); + } + + handleLoadMore = debounce(() => { + this.props.dispatch(expandDomainBlocks()); + }, 300, { leading: true }); + + render () { + const { intl, domains } = this.props; + + if (!domains) { + return ( + <Column> + <LoadingIndicator /> + </Column> + ); + } + + return ( + <Column icon='ban' heading={intl.formatMessage(messages.heading)}> + <ColumnBackButtonSlim /> + <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}> + {domains.map(domain => + <DomainContainer key={domain} domain={domain} /> + )} + </ScrollableList> + </Column> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/getting_started_misc/index.js b/app/javascript/flavours/glitch/features/getting_started_misc/index.js index 9cf7ddff9..77c44c273 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js @@ -14,6 +14,7 @@ const messages = defineMessages({ subheading: { id: 'column.subheading', defaultMessage: 'Miscellaneous options' }, favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, show_me_around: { id: 'getting_started.onboarding', defaultMessage: 'Show me around' }, @@ -49,6 +50,7 @@ export default class gettingStartedMisc extends ImmutablePureComponent { <ColumnLink key='20' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' /> <ColumnLink key='21' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' /> <ColumnLink key='22' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' /> + <ColumnLink icon='ban' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' /> <ColumnLink key='23' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' /> <ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> <ColumnLink icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} /> diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 0b031a7f0..6d9c14064 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -38,6 +38,7 @@ import { FavouritedStatuses, ListTimeline, Blocks, + DomainBlocks, Mutes, PinnedStatuses, Lists, @@ -60,6 +61,7 @@ const mapStateToProps = state => ({ layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), + dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, }); const keyMap = { @@ -111,6 +113,7 @@ export default class UI extends React.Component { hasComposingText: PropTypes.bool, location: PropTypes.object, intl: PropTypes.object.isRequired, + dropdownMenuIsOpen: PropTypes.bool, }; state = { @@ -366,7 +369,7 @@ export default class UI extends React.Component { render () { const { width, draggingOver } = this.state; - const { children, layout, isWide, navbarUnder } = this.props; + const { children, layout, isWide, navbarUnder, dropdownMenuIsOpen } = this.props; const columnsClass = layout => { switch (layout) { @@ -407,7 +410,7 @@ export default class UI extends React.Component { return ( <HotKeys keyMap={keyMap} handlers={handlers} ref={this.setHotkeysRef}> - <div className={className} ref={this.setRef}> + <div className={className} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}> {navbarUnder ? null : (<TabsBar />)} <ColumnsAreaContainer ref={this.setColumnsAreaRef} singleColumn={isMobile(width, layout)}> @@ -438,6 +441,7 @@ export default class UI extends React.Component { <WrappedRoute path='/follow_requests' component={FollowRequests} content={children} /> <WrappedRoute path='/blocks' component={Blocks} content={children} /> + <WrappedRoute path='/domain_blocks' component={DomainBlocks} content={children} /> <WrappedRoute path='/mutes' component={Mutes} content={children} /> <WrappedRoute path='/lists' component={Lists} content={children} /> <WrappedRoute path='/getting-started-misc' component={GettingStartedMisc} content={children} /> diff --git a/app/javascript/flavours/glitch/reducers/domain_lists.js b/app/javascript/flavours/glitch/reducers/domain_lists.js new file mode 100644 index 000000000..a9e3519f3 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/domain_lists.js @@ -0,0 +1,23 @@ +import { + DOMAIN_BLOCKS_FETCH_SUCCESS, + DOMAIN_BLOCKS_EXPAND_SUCCESS, + DOMAIN_UNBLOCK_SUCCESS, +} from '../actions/domain_blocks'; +import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; + +const initialState = ImmutableMap({ + blocks: ImmutableMap(), +}); + +export default function domainLists(state = initialState, action) { + switch(action.type) { + case DOMAIN_BLOCKS_FETCH_SUCCESS: + return state.setIn(['blocks', 'items'], ImmutableOrderedSet(action.domains)).setIn(['blocks', 'next'], action.next); + case DOMAIN_BLOCKS_EXPAND_SUCCESS: + return state.updateIn(['blocks', 'items'], set => set.union(action.domains)).setIn(['blocks', 'next'], action.next); + case DOMAIN_UNBLOCK_SUCCESS: + return state.updateIn(['blocks', 'items'], set => set.delete(action.domain)); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js new file mode 100644 index 000000000..5449884cc --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/dropdown_menu.js @@ -0,0 +1,18 @@ +import Immutable from 'immutable'; +import { + DROPDOWN_MENU_OPEN, + DROPDOWN_MENU_CLOSE, +} from '../actions/dropdown_menu'; + +const initialState = Immutable.Map({ openId: null, placement: null }); + +export default function dropdownMenu(state = initialState, action) { + switch (action.type) { + case DROPDOWN_MENU_OPEN: + return state.merge({ openId: action.id, placement: action.placement }); + case DROPDOWN_MENU_CLOSE: + return state.get('openId') === action.id ? state.set('openId', null) : state; + default: + return state; + } +} diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index e9c8d7c1d..dc8cb6834 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -1,10 +1,12 @@ import { combineReducers } from 'redux-immutable'; +import dropdown_menu from './dropdown_menu'; import timelines from './timelines'; import meta from './meta'; import alerts from './alerts'; import { loadingBarReducer } from 'react-redux-loading-bar'; import modal from './modal'; import user_lists from './user_lists'; +import domain_lists from './domain_lists'; import accounts from './accounts'; import accounts_counters from './accounts_counters'; import statuses from './statuses'; @@ -27,12 +29,14 @@ import lists from './lists'; import listEditor from './list_editor'; const reducers = { + dropdown_menu, timelines, meta, alerts, loadingBar: loadingBarReducer, modal, user_lists, + domain_lists, status_lists, accounts, accounts_counters, diff --git a/app/javascript/flavours/glitch/reducers/relationships.js b/app/javascript/flavours/glitch/reducers/relationships.js index 6303089ac..e5ef1d2e3 100644 --- a/app/javascript/flavours/glitch/reducers/relationships.js +++ b/app/javascript/flavours/glitch/reducers/relationships.js @@ -23,6 +23,14 @@ const normalizeRelationships = (state, relationships) => { return state; }; +const setDomainBlocking = (state, accounts, blocking) => { + return state.withMutations(map => { + accounts.forEach(id => { + map.setIn([id, 'domain_blocking'], blocking); + }); + }); +}; + const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { @@ -37,9 +45,9 @@ export default function relationships(state = initialState, action) { case RELATIONSHIPS_FETCH_SUCCESS: return normalizeRelationships(state, action.relationships); case DOMAIN_BLOCK_SUCCESS: - return state.setIn([action.accountId, 'domain_blocking'], true); + return setDomainBlocking(state, action.accounts, true); case DOMAIN_UNBLOCK_SUCCESS: - return state.setIn([action.accountId, 'domain_blocking'], false); + return setDomainBlocking(state, action.accounts, false); default: return state; } diff --git a/app/javascript/flavours/glitch/styles/components/domains.scss b/app/javascript/flavours/glitch/styles/components/domains.scss new file mode 100644 index 000000000..a99ccd02b --- /dev/null +++ b/app/javascript/flavours/glitch/styles/components/domains.scss @@ -0,0 +1,23 @@ +.domain { + padding: 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + .domain__domain-name { + flex: 1 1 auto; + display: block; + color: $primary-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + } +} + +.domain__wrapper { + display: flex; +} + +.domain_buttons { + height: 18px; + padding: 10px; + white-space: nowrap; +} diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 3d16c856d..aa33c9333 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1173,6 +1173,7 @@ noscript { @import 'boost'; @import 'accounts'; +@import 'domains'; @import 'status'; @import 'modal'; @import 'metadata'; diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 2aa9659e8..acda92d54 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -98,6 +98,10 @@ export function Blocks () { return import(/* webpackChunkName: "flavours/glitch/async/blocks" */'flavours/glitch/features/blocks'); } +export function DomainBlocks () { + return import(/* webpackChunkName: "flavours/glitch/async/domain_blocks" */'flavours/glitch/features/domain_blocks'); +} + export function Mutes () { return import(/* webpackChunkName: "flavours/glitch/async/mutes" */'flavours/glitch/features/mutes'); } |