diff options
Diffstat (limited to 'app/javascript/flavours')
26 files changed, 550 insertions, 101 deletions
diff --git a/app/javascript/flavours/glitch/actions/accounts.js b/app/javascript/flavours/glitch/actions/accounts.js index d5c4a02f9..d67ab112e 100644 --- a/app/javascript/flavours/glitch/actions/accounts.js +++ b/app/javascript/flavours/glitch/actions/accounts.js @@ -72,6 +72,17 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; +export const PINNED_ACCOUNTS_FETCH_REQUEST = 'PINNED_ACCOUNTS_FETCH_REQUEST'; +export const PINNED_ACCOUNTS_FETCH_SUCCESS = 'PINNED_ACCOUNTS_FETCH_SUCCESS'; +export const PINNED_ACCOUNTS_FETCH_FAIL = 'PINNED_ACCOUNTS_FETCH_FAIL'; + +export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY'; +export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR'; +export const PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE = 'PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE'; + +export const PINNED_ACCOUNTS_EDITOR_RESET = 'PINNED_ACCOUNTS_EDITOR_RESET'; + + export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -733,3 +744,76 @@ export function unpinAccountFail(error) { error, }; }; + +export function fetchPinnedAccounts() { + return (dispatch, getState) => { + dispatch(fetchPinnedAccountsRequest()); + + api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }) + .then(({ data }) => dispatch(fetchPinnedAccountsSuccess(data))) + .catch(err => dispatch(fetchPinnedAccountsFail(err))); + }; +}; + +export function fetchPinnedAccountsRequest() { + return { + type: PINNED_ACCOUNTS_FETCH_REQUEST, + }; +}; + +export function fetchPinnedAccountsSuccess(accounts, next) { + return { + type: PINNED_ACCOUNTS_FETCH_SUCCESS, + accounts, + next, + }; +}; + +export function fetchPinnedAccountsFail(error) { + return { + type: PINNED_ACCOUNTS_FETCH_FAIL, + error, + }; +}; + +export function fetchPinnedAccountsSuggestions(q) { + return (dispatch, getState) => { + const params = { + q, + resolve: false, + limit: 4, + following: true, + }; + + api(getState).get('/api/v1/accounts/search', { params }) + .then(({ data }) => dispatch(fetchPinnedAccountsSuggestionsReady(q, data))); + }; +}; + +export function fetchPinnedAccountsSuggestionsReady(query, accounts) { + return { + type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, + query, + accounts, + }; +}; + +export function clearPinnedAccountsSuggestions() { + return { + type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, + }; +}; + +export function changePinnedAccountsSuggestions(value) { + return { + type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE, + value, + } +}; + +export function resetPinnedAccountsEditor() { + return { + type: PINNED_ACCOUNTS_EDITOR_RESET, + }; +}; + diff --git a/app/javascript/flavours/glitch/actions/search.js b/app/javascript/flavours/glitch/actions/search.js index 13885c600..ec65bdf28 100644 --- a/app/javascript/flavours/glitch/actions/search.js +++ b/app/javascript/flavours/glitch/actions/search.js @@ -32,7 +32,7 @@ export function submitSearch() { dispatch(fetchSearchRequest()); - api(getState).get('/api/v1/search', { + api(getState).get('/api/v2/search', { params: { q: value, resolve: true, diff --git a/app/javascript/flavours/glitch/components/hashtag.js b/app/javascript/flavours/glitch/components/hashtag.js new file mode 100644 index 000000000..88689cc6c --- /dev/null +++ b/app/javascript/flavours/glitch/components/hashtag.js @@ -0,0 +1,34 @@ +import React from 'react'; +import { Sparklines, SparklinesCurve } from 'react-sparklines'; +import { Link } from 'react-router-dom'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { shortNumberFormat } from 'flavours/glitch/util/numbers'; + +const Hashtag = ({ hashtag }) => ( + <div className='trends__item'> + <div className='trends__item__name'> + <Link to={`/timelines/tag/${hashtag.get('name')}`}> + #<span>{hashtag.get('name')}</span> + </Link> + + <FormattedMessage id='trends.count_by_accounts' defaultMessage='{count} {rawCount, plural, one {person} other {people}} talking' values={{ rawCount: hashtag.getIn(['history', 0, 'accounts']), count: <strong>{shortNumberFormat(hashtag.getIn(['history', 0, 'accounts']))}</strong> }} /> + </div> + + <div className='trends__item__current'> + {shortNumberFormat(hashtag.getIn(['history', 0, 'uses']))} + </div> + + <div className='trends__item__sparkline'> + <Sparklines width={50} height={28} data={hashtag.get('history').reverse().map(day => day.get('uses')).toArray()}> + <SparklinesCurve style={{ fill: 'none' }} /> + </Sparklines> + </div> + </div> +); + +Hashtag.propTypes = { + hashtag: ImmutablePropTypes.map.isRequired, +}; + +export default Hashtag; diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index da1f74e6d..a87721ef8 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -528,6 +528,7 @@ export default class Status extends ImmutablePureComponent { {...other} status={status} account={status.get('account')} + showReplyCount={settings.get('show_reply_count')} /> ) : null} {notification ? ( diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index cd9a2ac67..8a840030a 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -33,6 +33,16 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, }); +const obfuscatedCount = count => { + if (count < 0) { + return 0; + } else if (count <= 1) { + return count; + } else { + return '1+'; + } +}; + @injectIntl export default class StatusActionBar extends ImmutablePureComponent { @@ -56,6 +66,7 @@ export default class StatusActionBar extends ImmutablePureComponent { onPin: PropTypes.func, onBookmark: PropTypes.func, withDismiss: PropTypes.bool, + showReplyCount: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -63,6 +74,7 @@ export default class StatusActionBar extends ImmutablePureComponent { // evaluate to false. See react-immutable-pure-component for usage. updateOnProps = [ 'status', + 'showReplyCount', 'withDismiss', ] @@ -134,7 +146,7 @@ export default class StatusActionBar extends ImmutablePureComponent { } render () { - const { status, intl, withDismiss } = this.props; + const { status, intl, withDismiss, showReplyCount } = this.props; const mutingConversation = status.get('muted'); const anonymousAccess = !me; @@ -188,9 +200,27 @@ export default class StatusActionBar extends ImmutablePureComponent { <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShareClick} /> ); + let replyButton = ( + <IconButton + className='status__action-bar-button' + disabled={anonymousAccess} + title={replyTitle} + icon={replyIcon} + onClick={this.handleReplyClick} + /> + ); + if (showReplyCount) { + replyButton = ( + <div className='status__action-bar__counter'> + {replyButton} + <span className='status__action-bar__counter__label' >{obfuscatedCount(status.get('replies_count'))}</span> + </div> + ); + } + return ( <div className='status__action-bar'> - <IconButton className='status__action-bar-button' disabled={anonymousAccess} title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /> + {replyButton} <IconButton className='status__action-bar-button' disabled={reblogDisabled} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(reblogMessage)} icon={reblogIcon} onClick={this.handleReblogClick} /> <IconButton className='status__action-bar-button star-icon' disabled={anonymousAccess} animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /> {shareButton} diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js index 23dc0e3cf..ac7a14ef4 100644 --- a/app/javascript/flavours/glitch/features/drawer/results/index.js +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -12,6 +12,7 @@ import { Link } from 'react-router-dom'; // Components. import AccountContainer from 'flavours/glitch/containers/account_container'; import StatusContainer from 'flavours/glitch/containers/status_container'; +import Hashtag from 'flavours/glitch/components/hashtag'; // Utils. import Motion from 'flavours/glitch/util/optional_motion'; @@ -98,15 +99,7 @@ export default function DrawerResults ({ <section> <h5><FormattedMessage id='search_results.hashtags' defaultMessage='Hashtags' /></h5> - {hashtags.map( - hashtag => ( - <Link - className='hashtag' - key={hashtag} - to={`/timelines/tag/${hashtag}`} - >#{hashtag}</Link> - ) - )} + {hashtags.map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)} </section> ) : null} </div> 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 b67e6f97f..ee4452472 100644 --- a/app/javascript/flavours/glitch/features/getting_started_misc/index.js +++ b/app/javascript/flavours/glitch/features/getting_started_misc/index.js @@ -21,6 +21,7 @@ const messages = defineMessages({ pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' }, info: { id: 'navigation_bar.info', defaultMessage: 'Extended information' }, keyboard_shortcuts: { id: 'navigation_bar.keyboard_shortcuts', defaultMessage: 'Keyboard shortcuts' }, + featured_users: { id: 'navigation_bar.featured_users', defaultMessage: 'Featured users' }, }); @connect() @@ -33,27 +34,33 @@ export default class gettingStartedMisc extends ImmutablePureComponent { }; openOnboardingModal = (e) => { - e.preventDefault(); this.props.dispatch(openModal('ONBOARDING')); } + openFeaturedAccountsModal = (e) => { + this.props.dispatch(openModal('PINNED_ACCOUNTS_EDITOR')); + } + render () { const { intl } = this.props; + let i = 1; + return ( <Column icon='ellipsis-h' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> <div className='scrollable'> <ColumnSubheading text={intl.formatMessage(messages.subheading)} /> - <ColumnLink key='19' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> - <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='minus-circle' 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} /> + <ColumnLink key='{i++}' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' /> + <ColumnLink key='{i++}' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' /> + <ColumnLink key='{i++}' icon='users' text={intl.formatMessage(messages.featured_users)} onClick={this.openFeaturedAccountsModal} /> + <ColumnLink key='{i++}' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' /> + <ColumnLink key='{i++}' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' /> + <ColumnLink key='{i++}' icon='minus-circle' text={intl.formatMessage(messages.domain_blocks)} to='/domain_blocks' /> + <ColumnLink key='{i++}' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' /> + <ColumnLink key='{i++}' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' /> + <ColumnLink key='{i++}' icon='hand-o-right' text={intl.formatMessage(messages.show_me_around)} onClick={this.openOnboardingModal} /> </div> </Column> ); diff --git a/app/javascript/flavours/glitch/features/list_editor/components/account.js b/app/javascript/flavours/glitch/features/list_editor/components/account.js index f48df759d..71a8b7673 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/account.js +++ b/app/javascript/flavours/glitch/features/list_editor/components/account.js @@ -1,38 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { makeGetAccount } from 'flavours/glitch/selectors'; import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Avatar from 'flavours/glitch/components/avatar'; import DisplayName from 'flavours/glitch/components/display_name'; import IconButton from 'flavours/glitch/components/icon_button'; -import { defineMessages, injectIntl } from 'react-intl'; -import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists'; +import { defineMessages } from 'react-intl'; const messages = defineMessages({ remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, add: { id: 'lists.account.add', defaultMessage: 'Add to list' }, }); -const makeMapStateToProps = () => { - const getAccount = makeGetAccount(); - - const mapStateToProps = (state, { accountId, added }) => ({ - account: getAccount(state, accountId), - added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added, - }); - - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch, { accountId }) => ({ - onRemove: () => dispatch(removeFromListEditor(accountId)), - onAdd: () => dispatch(addToListEditor(accountId)), -}); - -@connect(makeMapStateToProps, mapDispatchToProps) -@injectIntl export default class Account extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/list_editor/components/search.js b/app/javascript/flavours/glitch/features/list_editor/components/search.js index 45c4d0f2e..280632652 100644 --- a/app/javascript/flavours/glitch/features/list_editor/components/search.js +++ b/app/javascript/flavours/glitch/features/list_editor/components/search.js @@ -1,26 +1,12 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { defineMessages, injectIntl } from 'react-intl'; -import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; +import { defineMessages } from 'react-intl'; import classNames from 'classnames'; const messages = defineMessages({ search: { id: 'lists.search', defaultMessage: 'Search among people you follow' }, }); -const mapStateToProps = state => ({ - value: state.getIn(['listEditor', 'suggestions', 'value']), -}); - -const mapDispatchToProps = dispatch => ({ - onSubmit: value => dispatch(fetchListSuggestions(value)), - onClear: () => dispatch(clearListSuggestions()), - onChange: value => dispatch(changeListSuggestions(value)), -}); - -@connect(mapStateToProps, mapDispatchToProps) -@injectIntl export default class Search extends React.PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js new file mode 100644 index 000000000..782eb42f3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_editor/containers/account_container.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { injectIntl } from 'react-intl'; +import { removeFromListEditor, addToListEditor } from 'flavours/glitch/actions/lists'; +import Account from '../components/account'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId, added }) => ({ + account: getAccount(state, accountId), + added: typeof added === 'undefined' ? state.getIn(['listEditor', 'accounts', 'items']).includes(accountId) : added, + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { accountId }) => ({ + onRemove: () => dispatch(removeFromListEditor(accountId)), + onAdd: () => dispatch(addToListEditor(accountId)), +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); diff --git a/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js new file mode 100644 index 000000000..5af20efbd --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_editor/containers/search_container.js @@ -0,0 +1,17 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { injectIntl } from 'react-intl'; +import { fetchListSuggestions, clearListSuggestions, changeListSuggestions } from '../../../actions/lists'; +import Search from '../components/search'; + +const mapStateToProps = state => ({ + value: state.getIn(['listEditor', 'suggestions', 'value']), +}); + +const mapDispatchToProps = dispatch => ({ + onSubmit: value => dispatch(fetchListSuggestions(value)), + onClear: () => dispatch(clearListSuggestions()), + onChange: value => dispatch(changeListSuggestions(value)), +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search)); diff --git a/app/javascript/flavours/glitch/features/list_editor/index.js b/app/javascript/flavours/glitch/features/list_editor/index.js index e6df4755a..b3be3070a 100644 --- a/app/javascript/flavours/glitch/features/list_editor/index.js +++ b/app/javascript/flavours/glitch/features/list_editor/index.js @@ -5,8 +5,8 @@ import { connect } from 'react-redux'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { injectIntl } from 'react-intl'; import { setupListEditor, clearListSuggestions, resetListEditor } from 'flavours/glitch/actions/lists'; -import Account from './components/account'; -import Search from './components/search'; +import AccountContainer from './containers/account_container'; +import SearchContainer from './containers/search_container'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; @@ -56,11 +56,11 @@ export default class ListEditor extends ImmutablePureComponent { <div className='modal-root__modal list-editor'> <h4>{title}</h4> - <Search /> + <SearchContainer /> <div className='drawer__pager'> <div className='drawer__inner list-editor__accounts'> - {accountIds.map(accountId => <Account key={accountId} accountId={accountId} added />)} + {accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)} </div> {showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />} @@ -68,7 +68,7 @@ export default class ListEditor extends ImmutablePureComponent { <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> {({ x }) => (<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> - {searchAccountIds.map(accountId => <Account key={accountId} accountId={accountId} />)} + {searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)} </div>) } </Motion> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index d3b7b00b5..f88e23c47 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -35,34 +35,45 @@ export default class LocalSettingsPage extends React.PureComponent { <h1><FormattedMessage id='settings.general' defaultMessage='General' /></h1> <LocalSettingsPageItem settings={settings} - item={['layout']} - id='mastodon-settings--layout' - options={[ - { value: 'auto', message: intl.formatMessage(messages.layout_auto) }, - { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) }, - { value: 'single', message: intl.formatMessage(messages.layout_mobile) }, - ]} + item={['show_reply_count']} + id='mastodon-settings--reply-count' onChange={onChange} > - <FormattedMessage id='settings.layout' defaultMessage='Layout:' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['stretch']} - id='mastodon-settings--stretch' - onChange={onChange} - > - <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['navbar_under']} - id='mastodon-settings--navbar_under' - onChange={onChange} - > - <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> + <FormattedMessage id='settings.show_reply_counter' defaultMessage='Display an estimate of the reply count' /> </LocalSettingsPageItem> <section> + <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2> + <LocalSettingsPageItem + settings={settings} + item={['layout']} + id='mastodon-settings--layout' + options={[ + { value: 'auto', message: intl.formatMessage(messages.layout_auto) }, + { value: 'multiple', message: intl.formatMessage(messages.layout_desktop) }, + { value: 'single', message: intl.formatMessage(messages.layout_mobile) }, + ]} + onChange={onChange} + > + <FormattedMessage id='settings.layout' defaultMessage='Layout:' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['stretch']} + id='mastodon-settings--stretch' + onChange={onChange} + > + <FormattedMessage id='settings.wide_view' defaultMessage='Wide view (Desktop mode only)' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['navbar_under']} + id='mastodon-settings--navbar_under' + onChange={onChange} + > + <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> + </LocalSettingsPageItem> + </section> + <section> <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2> <LocalSettingsPageItem settings={settings} diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js new file mode 100644 index 000000000..149d05c32 --- /dev/null +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/account_container.js @@ -0,0 +1,24 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import { injectIntl } from 'react-intl'; +import { pinAccount, unpinAccount } from 'flavours/glitch/actions/accounts'; +import Account from 'flavours/glitch/features/list_editor/components/account'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId, added }) => ({ + account: getAccount(state, accountId), + added: typeof added === 'undefined' ? state.getIn(['pinnedAccountsEditor', 'accounts', 'items']).includes(accountId) : added, + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { accountId }) => ({ + onRemove: () => dispatch(unpinAccount(accountId)), + onAdd: () => dispatch(pinAccount(accountId)), +}); + +export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Account)); diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js new file mode 100644 index 000000000..5a1efce0a --- /dev/null +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/containers/search_container.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { injectIntl } from 'react-intl'; +import { + fetchPinnedAccountsSuggestions, + clearPinnedAccountsSuggestions, + changePinnedAccountsSuggestions +} from '../../../actions/accounts'; +import Search from 'flavours/glitch/features/list_editor/components/search'; + +const mapStateToProps = state => ({ + value: state.getIn(['pinnedAccountsEditor', 'suggestions', 'value']), +}); + +const mapDispatchToProps = dispatch => ({ + onSubmit: value => dispatch(fetchPinnedAccountsSuggestions(value)), + onClear: () => dispatch(clearPinnedAccountsSuggestions()), + onChange: value => dispatch(changePinnedAccountsSuggestions(value)), +}); + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Search)); diff --git a/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js new file mode 100644 index 000000000..7484e458e --- /dev/null +++ b/app/javascript/flavours/glitch/features/pinned_accounts_editor/index.js @@ -0,0 +1,78 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { injectIntl, FormattedMessage } from 'react-intl'; +import { fetchPinnedAccounts, clearPinnedAccountsSuggestions, resetPinnedAccountsEditor } from 'flavours/glitch/actions/accounts'; +import AccountContainer from './containers/account_container'; +import SearchContainer from './containers/search_container'; +import Motion from 'flavours/glitch/util/optional_motion'; +import spring from 'react-motion/lib/spring'; + +const mapStateToProps = state => ({ + accountIds: state.getIn(['pinnedAccountsEditor', 'accounts', 'items']), + searchAccountIds: state.getIn(['pinnedAccountsEditor', 'suggestions', 'items']), +}); + +const mapDispatchToProps = dispatch => ({ + onInitialize: () => dispatch(fetchPinnedAccounts()), + onClear: () => dispatch(clearPinnedAccountsSuggestions()), + onReset: () => dispatch(resetPinnedAccountsEditor()), +}); + +@connect(mapStateToProps, mapDispatchToProps) +@injectIntl +export default class PinnedAccountsEditor extends ImmutablePureComponent { + + static propTypes = { + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + onInitialize: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + onReset: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + accountIds: ImmutablePropTypes.list.isRequired, + searchAccountIds: ImmutablePropTypes.list.isRequired, + }; + + componentDidMount () { + const { onInitialize } = this.props; + onInitialize(); + } + + componentWillUnmount () { + const { onReset } = this.props; + onReset(); + } + + render () { + const { accountIds, searchAccountIds, onClear } = this.props; + const showSearch = searchAccountIds.size > 0; + + return ( + <div className='modal-root__modal list-editor'> + <h4><FormattedMessage id='endorsed_accounts_editor.endorsed_accounts' defaultMessage='Featured accounts' /></h4> + + <SearchContainer /> + + <div className='drawer__pager'> + <div className='drawer__inner list-editor__accounts'> + {accountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} added />)} + </div> + + {showSearch && <div role='button' tabIndex='-1' className='drawer__backdrop' onClick={onClear} />} + + <Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> + {({ x }) => + (<div className='drawer__inner backdrop' style={{ transform: x === 0 ? null : `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> + {searchAccountIds.map(accountId => <AccountContainer key={accountId} accountId={accountId} />)} + </div>) + } + </Motion> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index 23a7603d8..c9f54804a 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -19,6 +19,7 @@ import { SettingsModal, EmbedModal, ListEditor, + PinnedAccountsEditor, } from 'flavours/glitch/util/async-components'; const MODAL_COMPONENTS = { @@ -36,6 +37,7 @@ const MODAL_COMPONENTS = { 'EMBED': EmbedModal, 'LIST_EDITOR': ListEditor, 'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }), + 'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor, }; export default class ModalRoot extends React.PureComponent { diff --git a/app/javascript/flavours/glitch/reducers/accounts.js b/app/javascript/flavours/glitch/reducers/accounts.js index c38b3cc95..c2f016a87 100644 --- a/app/javascript/flavours/glitch/reducers/accounts.js +++ b/app/javascript/flavours/glitch/reducers/accounts.js @@ -6,6 +6,8 @@ import { FOLLOWING_EXPAND_SUCCESS, FOLLOW_REQUESTS_FETCH_SUCCESS, FOLLOW_REQUESTS_EXPAND_SUCCESS, + PINNED_ACCOUNTS_FETCH_SUCCESS, + PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, } from 'flavours/glitch/actions/accounts'; import { BLOCKS_FETCH_SUCCESS, @@ -141,6 +143,8 @@ export default function accounts(state = initialState, action) { case MUTES_EXPAND_SUCCESS: case LIST_ACCOUNTS_FETCH_SUCCESS: case LIST_EDITOR_SUGGESTIONS_READY: + case PINNED_ACCOUNTS_FETCH_SUCCESS: + case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY: return action.accounts ? normalizeAccounts(state, action.accounts) : state; case NOTIFICATIONS_EXPAND_SUCCESS: case SEARCH_FETCH_SUCCESS: diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index 7b7bc2ca2..218a5ac8f 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -28,6 +28,7 @@ import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; import filters from './filters'; +import pinnedAccountsEditor from './pinned_accounts_editor'; const reducers = { dropdown_menu, @@ -59,6 +60,7 @@ const reducers = { lists, listEditor, filters, + pinnedAccountsEditor, }; export default combineReducers(reducers); diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 73d034dbe..51032f345 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -11,6 +11,7 @@ const initialState = ImmutableMap({ navbar_under : false, side_arm : 'none', side_arm_reply_mode : 'keep', + show_reply_count : false, collapsed : ImmutableMap({ enabled : true, auto : ImmutableMap({ diff --git a/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js new file mode 100644 index 000000000..267521bb8 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/pinned_accounts_editor.js @@ -0,0 +1,57 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { + PINNED_ACCOUNTS_EDITOR_RESET, + PINNED_ACCOUNTS_FETCH_REQUEST, + PINNED_ACCOUNTS_FETCH_SUCCESS, + PINNED_ACCOUNTS_FETCH_FAIL, + PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY, + PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR, + PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE, + ACCOUNT_PIN_SUCCESS, + ACCOUNT_UNPIN_SUCCESS, +} from '../actions/accounts'; + +const initialState = ImmutableMap({ + accounts: ImmutableMap({ + items: ImmutableList(), + loaded: false, + isLoading: false, + }), + + suggestions: ImmutableMap({ + value: '', + items: ImmutableList(), + }), +}); + +export default function listEditorReducer(state = initialState, action) { + switch(action.type) { + case PINNED_ACCOUNTS_EDITOR_RESET: + return initialState; + case PINNED_ACCOUNTS_FETCH_REQUEST: + return state.setIn(['accounts', 'isLoading'], true); + case PINNED_ACCOUNTS_FETCH_FAIL: + return state.setIn(['accounts', 'isLoading'], false); + case PINNED_ACCOUNTS_FETCH_SUCCESS: + return state.update('accounts', accounts => accounts.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set('items', ImmutableList(action.accounts.map(item => item.id))); + })); + case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE: + return state.setIn(['suggestions', 'value'], action.value); + case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_READY: + return state.setIn(['suggestions', 'items'], ImmutableList(action.accounts.map(item => item.id))); + case PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CLEAR: + return state.update('suggestions', suggestions => suggestions.withMutations(map => { + map.set('items', ImmutableList()); + map.set('value', ''); + })); + case ACCOUNT_PIN_SUCCESS: + return state.updateIn(['accounts', 'items'], list => list.unshift(action.relationship.id)); + case ACCOUNT_UNPIN_SUCCESS: + return state.updateIn(['accounts', 'items'], list => list.filterNot(item => item === action.relationship.id)); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/search.js b/app/javascript/flavours/glitch/reducers/search.js index dc6be97e2..9a525bf47 100644 --- a/app/javascript/flavours/glitch/reducers/search.js +++ b/app/javascript/flavours/glitch/reducers/search.js @@ -9,7 +9,7 @@ import { COMPOSE_REPLY, COMPOSE_DIRECT, } from 'flavours/glitch/actions/compose'; -import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; const initialState = ImmutableMap({ value: '', @@ -39,7 +39,7 @@ export default function search(state = initialState, action) { return state.set('results', ImmutableMap({ accounts: ImmutableList(action.results.accounts.map(item => item.id)), statuses: ImmutableList(action.results.statuses.map(item => item.id)), - hashtags: ImmutableList(action.results.hashtags), + hashtags: fromJS(action.results.hashtags), })).set('submitted', true); default: return state; diff --git a/app/javascript/flavours/glitch/styles/components/search.scss b/app/javascript/flavours/glitch/styles/components/search.scss index 91861ea19..f9e4b5883 100644 --- a/app/javascript/flavours/glitch/styles/components/search.scss +++ b/app/javascript/flavours/glitch/styles/components/search.scss @@ -90,16 +90,80 @@ font-weight: 500; } -.search-results__hashtag { - display: block; - padding: 10px; - color: $secondary-text-color; - text-decoration: none; +.trends { + &__header { + color: $dark-text-color; + background: lighten($ui-base-color, 2%); + border-bottom: 1px solid darken($ui-base-color, 4%); + font-weight: 500; + padding: 15px; + font-size: 16px; + cursor: default; - &:hover, - &:active, - &:focus { - color: lighten($secondary-text-color, 4%); - text-decoration: underline; + .fa { + display: inline-block; + margin-right: 5px; + } + } + + &__item { + display: flex; + align-items: center; + padding: 15px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + + &:last-child { + border-bottom: 0; + } + + &__name { + flex: 1 1 auto; + color: $dark-text-color; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + strong { + font-weight: 500; + } + + a { + color: $darker-text-color; + text-decoration: none; + font-size: 14px; + font-weight: 500; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover, + &:focus, + &:active { + span { + text-decoration: underline; + } + } + } + } + + &__current { + flex: 0 0 auto; + width: 100px; + font-size: 24px; + line-height: 36px; + font-weight: 500; + text-align: center; + color: $secondary-text-color; + } + + &__sparkline { + flex: 0 0 auto; + width: 50px; + + path { + stroke: lighten($highlight-text-color, 6%) !important; + } + } } } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index cd17bb4fa..fbc26ed2a 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -417,15 +417,31 @@ align-items: center; display: flex; margin-top: 8px; + + &__counter { + display: inline-flex; + margin-right: 11px; + align-items: center; + + .status__action-bar-button { + margin-right: 4px; + } + + &__label { + display: inline-block; + width: 14px; + font-size: 12px; + font-weight: 500; + color: $action-button-color; + } + } } .status__action-bar-button { - float: left; margin-right: 18px; } .status__action-bar-dropdown { - float: left; height: 23.15px; width: 23.15px; } diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 3d6d3d1f4..557ce317e 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -38,6 +38,10 @@ export function ListEditor () { return import(/* webpackChunkName: "flavours/glitch/async/list_editor" */'flavours/glitch/features/list_editor'); } +export function PinnedAccountsEditor () { + return import(/* webpackChunkName: "flavours/glitch/async/pinned_accounts_editor" */'flavours/glitch/features/pinned_accounts_editor'); +} + export function DirectTimeline() { return import(/* webpackChunkName: "flavours/glitch/async/direct_timeline" */'flavours/glitch/features/direct_timeline'); } diff --git a/app/javascript/flavours/glitch/util/numbers.js b/app/javascript/flavours/glitch/util/numbers.js new file mode 100644 index 000000000..fdd8269ae --- /dev/null +++ b/app/javascript/flavours/glitch/util/numbers.js @@ -0,0 +1,10 @@ +import React, { Fragment } from 'react'; +import { FormattedNumber } from 'react-intl'; + +export const shortNumberFormat = number => { + if (number < 1000) { + return <FormattedNumber value={number} />; + } else { + return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>; + } +}; |