diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features/directory')
-rw-r--r-- | app/javascript/flavours/glitch/features/directory/components/account_card.js | 271 | ||||
-rw-r--r-- | app/javascript/flavours/glitch/features/directory/index.js | 170 |
2 files changed, 441 insertions, 0 deletions
diff --git a/app/javascript/flavours/glitch/features/directory/components/account_card.js b/app/javascript/flavours/glitch/features/directory/components/account_card.js new file mode 100644 index 000000000..2a3fd1ecf --- /dev/null +++ b/app/javascript/flavours/glitch/features/directory/components/account_card.js @@ -0,0 +1,271 @@ +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { makeGetAccount } from 'flavours/glitch/selectors'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import Permalink from 'flavours/glitch/components/permalink'; +import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; +import IconButton from 'flavours/glitch/components/icon_button'; +import { FormattedMessage, injectIntl, defineMessages } from 'react-intl'; +import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/util/initial_state'; +import ShortNumber from 'flavours/glitch/components/short_number'; +import { + followAccount, + unfollowAccount, + blockAccount, + unblockAccount, + unmuteAccount, +} from 'flavours/glitch/actions/accounts'; +import { openModal } from 'flavours/glitch/actions/modal'; +import { initMuteModal } from 'flavours/glitch/actions/mutes'; + +const messages = defineMessages({ + follow: { id: 'account.follow', defaultMessage: 'Follow' }, + unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, + requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' }, + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + unfollowConfirm: { + id: 'confirmations.unfollow.confirm', + defaultMessage: 'Unfollow', + }, +}); + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { id }) => ({ + account: getAccount(state, id), + }); + + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onFollow(account) { + if ( + account.getIn(['relationship', 'following']) || + account.getIn(['relationship', 'requested']) + ) { + if (unfollowModal) { + dispatch( + openModal('CONFIRM', { + message: ( + <FormattedMessage + id='confirmations.unfollow.message' + defaultMessage='Are you sure you want to unfollow {name}?' + values={{ name: <strong>@{account.get('acct')}</strong> }} + /> + ), + confirm: intl.formatMessage(messages.unfollowConfirm), + onConfirm: () => dispatch(unfollowAccount(account.get('id'))), + }), + ); + } else { + dispatch(unfollowAccount(account.get('id'))); + } + } else { + dispatch(followAccount(account.get('id'))); + } + }, + + onBlock(account) { + if (account.getIn(['relationship', 'blocking'])) { + dispatch(unblockAccount(account.get('id'))); + } else { + dispatch(blockAccount(account.get('id'))); + } + }, + + onMute(account) { + if (account.getIn(['relationship', 'muting'])) { + dispatch(unmuteAccount(account.get('id'))); + } else { + dispatch(initMuteModal(account)); + } + }, +}); + +export default +@injectIntl +@connect(makeMapStateToProps, mapDispatchToProps) +class AccountCard extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + onFollow: PropTypes.func.isRequired, + onBlock: PropTypes.func.isRequired, + onMute: PropTypes.func.isRequired, + }; + + handleMouseEnter = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-original'); + } + } + + handleMouseLeave = ({ currentTarget }) => { + if (autoPlayGif) { + return; + } + + const emojis = currentTarget.querySelectorAll('.custom-emoji'); + + for (var i = 0; i < emojis.length; i++) { + let emoji = emojis[i]; + emoji.src = emoji.getAttribute('data-static'); + } + } + + handleFollow = () => { + this.props.onFollow(this.props.account); + }; + + handleBlock = () => { + this.props.onBlock(this.props.account); + }; + + handleMute = () => { + this.props.onMute(this.props.account); + }; + + render() { + const { account, intl } = this.props; + + let buttons; + + if ( + account.get('id') !== me && + account.get('relationship', null) !== null + ) { + const following = account.getIn(['relationship', 'following']); + const requested = account.getIn(['relationship', 'requested']); + const blocking = account.getIn(['relationship', 'blocking']); + const muting = account.getIn(['relationship', 'muting']); + + if (requested) { + buttons = ( + <IconButton + disabled + icon='hourglass' + title={intl.formatMessage(messages.requested)} + /> + ); + } else if (blocking) { + buttons = ( + <IconButton + active + icon='unlock' + title={intl.formatMessage(messages.unblock, { + name: account.get('username'), + })} + onClick={this.handleBlock} + /> + ); + } else if (muting) { + buttons = ( + <IconButton + active + icon='volume-up' + title={intl.formatMessage(messages.unmute, { + name: account.get('username'), + })} + onClick={this.handleMute} + /> + ); + } else if (!account.get('moved') || following) { + buttons = ( + <IconButton + icon={following ? 'user-times' : 'user-plus'} + title={intl.formatMessage( + following ? messages.unfollow : messages.follow, + )} + onClick={this.handleFollow} + active={following} + /> + ); + } + } + + return ( + <div className='directory__card'> + <div className='directory__card__img'> + <img + src={ + autoPlayGif ? account.get('header') : account.get('header_static') + } + alt='' + /> + </div> + + <div className='directory__card__bar'> + <Permalink + className='directory__card__bar__name' + href={account.get('url')} + to={`/@${account.get('acct')}`} + > + <Avatar account={account} size={48} /> + <DisplayName account={account} /> + </Permalink> + + <div className='directory__card__bar__relationship account__relationship'> + {buttons} + </div> + </div> + + <div className='directory__card__extra' onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + <div + className='account__header__content translate' + dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }} + /> + </div> + + <div className='directory__card__extra'> + <div className='accounts-table__count'> + <ShortNumber value={account.get('statuses_count')} /> + <small> + <FormattedMessage id='account.posts' defaultMessage='Toots' /> + </small> + </div> + <div className='accounts-table__count'> + {account.get('followers_count') < 0 ? '-' : <ShortNumber value={account.get('followers_count')} />}{' '} + <small> + <FormattedMessage + id='account.followers' + defaultMessage='Followers' + /> + </small> + </div> + <div className='accounts-table__count'> + {account.get('last_status_at') === null ? ( + <FormattedMessage + id='account.never_active' + defaultMessage='Never' + /> + ) : ( + <RelativeTimestamp timestamp={account.get('last_status_at')} /> + )}{' '} + <small> + <FormattedMessage + id='account.last_status' + defaultMessage='Last active' + /> + </small> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/directory/index.js b/app/javascript/flavours/glitch/features/directory/index.js new file mode 100644 index 000000000..cde5926e0 --- /dev/null +++ b/app/javascript/flavours/glitch/features/directory/index.js @@ -0,0 +1,170 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Column from 'flavours/glitch/components/column'; +import ColumnHeader from 'flavours/glitch/components/column_header'; +import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'flavours/glitch/actions/columns'; +import { fetchDirectory, expandDirectory } from 'flavours/glitch/actions/directory'; +import { List as ImmutableList } from 'immutable'; +import AccountCard from './components/account_card'; +import RadioButton from 'flavours/glitch/components/radio_button'; +import classNames from 'classnames'; +import LoadMore from 'flavours/glitch/components/load_more'; +import ScrollContainer from 'flavours/glitch/containers/scroll_container'; + +const messages = defineMessages({ + title: { id: 'column.directory', defaultMessage: 'Browse profiles' }, + recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' }, + newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' }, + local: { id: 'directory.local', defaultMessage: 'From {domain} only' }, + federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' }, +}); + +const mapStateToProps = state => ({ + accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()), + isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true), + domain: state.getIn(['meta', 'domain']), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Directory extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + isLoading: PropTypes.bool, + accountIds: ImmutablePropTypes.list.isRequired, + dispatch: PropTypes.func.isRequired, + columnId: PropTypes.string, + intl: PropTypes.object.isRequired, + multiColumn: PropTypes.bool, + domain: PropTypes.string.isRequired, + params: PropTypes.shape({ + order: PropTypes.string, + local: PropTypes.bool, + }), + }; + + state = { + order: null, + local: null, + }; + + handlePin = () => { + const { columnId, dispatch } = this.props; + + if (columnId) { + dispatch(removeColumn(columnId)); + } else { + dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state))); + } + } + + getParams = (props, state) => ({ + order: state.order === null ? (props.params.order || 'active') : state.order, + local: state.local === null ? (props.params.local || false) : state.local, + }); + + handleMove = dir => { + const { columnId, dispatch } = this.props; + dispatch(moveColumn(columnId, dir)); + } + + handleHeaderClick = () => { + this.column.scrollTop(); + } + + componentDidMount () { + const { dispatch } = this.props; + dispatch(fetchDirectory(this.getParams(this.props, this.state))); + } + + componentDidUpdate (prevProps, prevState) { + const { dispatch } = this.props; + const paramsOld = this.getParams(prevProps, prevState); + const paramsNew = this.getParams(this.props, this.state); + + if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) { + dispatch(fetchDirectory(paramsNew)); + } + } + + setRef = c => { + this.column = c; + } + + handleChangeOrder = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['order'], e.target.value)); + } else { + this.setState({ order: e.target.value }); + } + } + + handleChangeLocal = e => { + const { dispatch, columnId } = this.props; + + if (columnId) { + dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1')); + } else { + this.setState({ local: e.target.value === '1' }); + } + } + + handleLoadMore = () => { + const { dispatch } = this.props; + dispatch(expandDirectory(this.getParams(this.props, this.state))); + } + + render () { + const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props; + const { order, local } = this.getParams(this.props, this.state); + const pinned = !!columnId; + + const scrollableArea = ( + <div className='scrollable' style={{ background: 'transparent' }}> + <div className='filter-form'> + <div className='filter-form__column' role='group'> + <RadioButton name='order' value='active' label={intl.formatMessage(messages.recentlyActive)} checked={order === 'active'} onChange={this.handleChangeOrder} /> + <RadioButton name='order' value='new' label={intl.formatMessage(messages.newArrivals)} checked={order === 'new'} onChange={this.handleChangeOrder} /> + </div> + + <div className='filter-form__column' role='group'> + <RadioButton name='local' value='1' label={intl.formatMessage(messages.local, { domain })} checked={local} onChange={this.handleChangeLocal} /> + <RadioButton name='local' value='0' label={intl.formatMessage(messages.federated)} checked={!local} onChange={this.handleChangeLocal} /> + </div> + </div> + + <div className={classNames('directory__list', { loading: isLoading })}> + {accountIds.map(accountId => <AccountCard id={accountId} key={accountId} />)} + </div> + + <LoadMore onClick={this.handleLoadMore} visible={!isLoading} /> + </div> + ); + + return ( + <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> + <ColumnHeader + icon='address-book-o' + title={intl.formatMessage(messages.title)} + onPin={this.handlePin} + onMove={this.handleMove} + onClick={this.handleHeaderClick} + pinned={pinned} + multiColumn={multiColumn} + /> + + {multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea} + </Column> + ); + } + +} |