Convert <Directory> to Typescript / function component (#30829)
				
					
				
			This commit is contained in:
		
							parent
							
								
									51f581e03e
								
							
						
					
					
						commit
						863c470a2b
					
				@ -1,62 +0,0 @@
 | 
			
		||||
import api from '../api';
 | 
			
		||||
 | 
			
		||||
import { fetchRelationships } from './accounts';
 | 
			
		||||
import { importFetchedAccounts } from './importer';
 | 
			
		||||
 | 
			
		||||
export const DIRECTORY_FETCH_REQUEST = 'DIRECTORY_FETCH_REQUEST';
 | 
			
		||||
export const DIRECTORY_FETCH_SUCCESS = 'DIRECTORY_FETCH_SUCCESS';
 | 
			
		||||
export const DIRECTORY_FETCH_FAIL    = 'DIRECTORY_FETCH_FAIL';
 | 
			
		||||
 | 
			
		||||
export const DIRECTORY_EXPAND_REQUEST = 'DIRECTORY_EXPAND_REQUEST';
 | 
			
		||||
export const DIRECTORY_EXPAND_SUCCESS = 'DIRECTORY_EXPAND_SUCCESS';
 | 
			
		||||
export const DIRECTORY_EXPAND_FAIL    = 'DIRECTORY_EXPAND_FAIL';
 | 
			
		||||
 | 
			
		||||
export const fetchDirectory = params => (dispatch) => {
 | 
			
		||||
  dispatch(fetchDirectoryRequest());
 | 
			
		||||
 | 
			
		||||
  api().get('/api/v1/directory', { params: { ...params, limit: 20 } }).then(({ data }) => {
 | 
			
		||||
    dispatch(importFetchedAccounts(data));
 | 
			
		||||
    dispatch(fetchDirectorySuccess(data));
 | 
			
		||||
    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
			
		||||
  }).catch(error => dispatch(fetchDirectoryFail(error)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fetchDirectoryRequest = () => ({
 | 
			
		||||
  type: DIRECTORY_FETCH_REQUEST,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const fetchDirectorySuccess = accounts => ({
 | 
			
		||||
  type: DIRECTORY_FETCH_SUCCESS,
 | 
			
		||||
  accounts,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const fetchDirectoryFail = error => ({
 | 
			
		||||
  type: DIRECTORY_FETCH_FAIL,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const expandDirectory = params => (dispatch, getState) => {
 | 
			
		||||
  dispatch(expandDirectoryRequest());
 | 
			
		||||
 | 
			
		||||
  const loadedItems = getState().getIn(['user_lists', 'directory', 'items']).size;
 | 
			
		||||
 | 
			
		||||
  api().get('/api/v1/directory', { params: { ...params, offset: loadedItems, limit: 20 } }).then(({ data }) => {
 | 
			
		||||
    dispatch(importFetchedAccounts(data));
 | 
			
		||||
    dispatch(expandDirectorySuccess(data));
 | 
			
		||||
    dispatch(fetchRelationships(data.map(x => x.id)));
 | 
			
		||||
  }).catch(error => dispatch(expandDirectoryFail(error)));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const expandDirectoryRequest = () => ({
 | 
			
		||||
  type: DIRECTORY_EXPAND_REQUEST,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const expandDirectorySuccess = accounts => ({
 | 
			
		||||
  type: DIRECTORY_EXPAND_SUCCESS,
 | 
			
		||||
  accounts,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const expandDirectoryFail = error => ({
 | 
			
		||||
  type: DIRECTORY_EXPAND_FAIL,
 | 
			
		||||
  error,
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										37
									
								
								app/javascript/mastodon/actions/directory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								app/javascript/mastodon/actions/directory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import type { List as ImmutableList } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import { apiGetDirectory } from 'mastodon/api/directory';
 | 
			
		||||
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
 | 
			
		||||
 | 
			
		||||
import { fetchRelationships } from './accounts';
 | 
			
		||||
import { importFetchedAccounts } from './importer';
 | 
			
		||||
 | 
			
		||||
export const fetchDirectory = createDataLoadingThunk(
 | 
			
		||||
  'directory/fetch',
 | 
			
		||||
  async (params: Parameters<typeof apiGetDirectory>[0]) =>
 | 
			
		||||
    apiGetDirectory(params),
 | 
			
		||||
  (data, { dispatch }) => {
 | 
			
		||||
    dispatch(importFetchedAccounts(data));
 | 
			
		||||
    dispatch(fetchRelationships(data.map((x) => x.id)));
 | 
			
		||||
 | 
			
		||||
    return { accounts: data };
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export const expandDirectory = createDataLoadingThunk(
 | 
			
		||||
  'directory/expand',
 | 
			
		||||
  async (params: Parameters<typeof apiGetDirectory>[0], { getState }) => {
 | 
			
		||||
    const loadedItems = getState().user_lists.getIn([
 | 
			
		||||
      'directory',
 | 
			
		||||
      'items',
 | 
			
		||||
    ]) as ImmutableList<unknown>;
 | 
			
		||||
 | 
			
		||||
    return apiGetDirectory({ ...params, offset: loadedItems.size }, 20);
 | 
			
		||||
  },
 | 
			
		||||
  (data, { dispatch }) => {
 | 
			
		||||
    dispatch(importFetchedAccounts(data));
 | 
			
		||||
    dispatch(fetchRelationships(data.map((x) => x.id)));
 | 
			
		||||
 | 
			
		||||
    return { accounts: data };
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
							
								
								
									
										15
									
								
								app/javascript/mastodon/api/directory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/javascript/mastodon/api/directory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { apiRequestGet } from 'mastodon/api';
 | 
			
		||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
 | 
			
		||||
 | 
			
		||||
export const apiGetDirectory = (
 | 
			
		||||
  params: {
 | 
			
		||||
    order: string;
 | 
			
		||||
    local: boolean;
 | 
			
		||||
    offset?: number;
 | 
			
		||||
  },
 | 
			
		||||
  limit = 20,
 | 
			
		||||
) =>
 | 
			
		||||
  apiRequestGet<ApiAccountJSON[]>('v1/directory', {
 | 
			
		||||
    ...params,
 | 
			
		||||
    limit,
 | 
			
		||||
  });
 | 
			
		||||
@ -1,234 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import ImmutablePureComponent from 'react-immutable-pure-component';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  followAccount,
 | 
			
		||||
  unfollowAccount,
 | 
			
		||||
  unblockAccount,
 | 
			
		||||
  unmuteAccount,
 | 
			
		||||
} from 'mastodon/actions/accounts';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { Avatar } from 'mastodon/components/avatar';
 | 
			
		||||
import { Button } from 'mastodon/components/button';
 | 
			
		||||
import { DisplayName } from 'mastodon/components/display_name';
 | 
			
		||||
import { ShortNumber } from 'mastodon/components/short_number';
 | 
			
		||||
import { autoPlayGif, me } from 'mastodon/initial_state';
 | 
			
		||||
import { makeGetAccount } from 'mastodon/selectors';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
			
		||||
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
			
		||||
  cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw follow request' },
 | 
			
		||||
  cancelFollowRequestConfirm: { id: 'confirmations.cancel_follow_request.confirm', defaultMessage: 'Withdraw request' },
 | 
			
		||||
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
 | 
			
		||||
  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
 | 
			
		||||
  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
 | 
			
		||||
  unfollowConfirm: { id: 'confirmations.unfollow.confirm', defaultMessage: 'Unfollow' },
 | 
			
		||||
  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
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'])) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        openModal({
 | 
			
		||||
          modalType: 'CONFIRM',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            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 if (account.getIn(['relationship', 'requested'])) {
 | 
			
		||||
      dispatch(openModal({
 | 
			
		||||
        modalType: 'CONFIRM',
 | 
			
		||||
        modalProps: {
 | 
			
		||||
          message: <FormattedMessage id='confirmations.cancel_follow_request.message' defaultMessage='Are you sure you want to withdraw your request to follow {name}?' values={{ name: <strong>@{account.get('acct')}</strong> }} />,
 | 
			
		||||
          confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
 | 
			
		||||
          onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
 | 
			
		||||
        },
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(followAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onBlock(account) {
 | 
			
		||||
    if (account.getIn(['relationship', 'blocking'])) {
 | 
			
		||||
      dispatch(unblockAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  onMute(account) {
 | 
			
		||||
    if (account.getIn(['relationship', 'muting'])) {
 | 
			
		||||
      dispatch(unmuteAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class AccountCard extends ImmutablePureComponent {
 | 
			
		||||
 | 
			
		||||
  static propTypes = {
 | 
			
		||||
    account: ImmutablePropTypes.record.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);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  handleEditProfile = () => {
 | 
			
		||||
    window.open('/settings/profile', '_blank');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { account, intl } = this.props;
 | 
			
		||||
 | 
			
		||||
    let actionBtn;
 | 
			
		||||
 | 
			
		||||
    if (me !== account.get('id')) {
 | 
			
		||||
      if (!account.get('relationship')) { // Wait until the relationship is loaded
 | 
			
		||||
        actionBtn = '';
 | 
			
		||||
      } else if (account.getIn(['relationship', 'requested'])) {
 | 
			
		||||
        actionBtn = <Button  text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.handleFollow} />;
 | 
			
		||||
      } else if (account.getIn(['relationship', 'muting'])) {
 | 
			
		||||
        actionBtn = <Button  text={intl.formatMessage(messages.unmute)} onClick={this.handleMute} />;
 | 
			
		||||
      } else if (!account.getIn(['relationship', 'blocking'])) {
 | 
			
		||||
        actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.handleFollow} />;
 | 
			
		||||
      } else if (account.getIn(['relationship', 'blocking'])) {
 | 
			
		||||
        actionBtn = <Button  text={intl.formatMessage(messages.unblock)} onClick={this.handleBlock} />;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      actionBtn = <Button  text={intl.formatMessage(messages.edit_profile)} onClick={this.handleEditProfile} />;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className='account-card'>
 | 
			
		||||
        <Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
 | 
			
		||||
          <div className='account-card__header'>
 | 
			
		||||
            <img
 | 
			
		||||
              src={
 | 
			
		||||
                autoPlayGif ? account.get('header') : account.get('header_static')
 | 
			
		||||
              }
 | 
			
		||||
              alt=''
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account-card__title'>
 | 
			
		||||
            <div className='account-card__title__avatar'><Avatar account={account} size={56} /></div>
 | 
			
		||||
            <DisplayName account={account} />
 | 
			
		||||
          </div>
 | 
			
		||||
        </Link>
 | 
			
		||||
 | 
			
		||||
        {account.get('note').length > 0 && (
 | 
			
		||||
          <div
 | 
			
		||||
            className='account-card__bio translate'
 | 
			
		||||
            onMouseEnter={this.handleMouseEnter}
 | 
			
		||||
            onMouseLeave={this.handleMouseLeave}
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <div className='account-card__actions'>
 | 
			
		||||
          <div className='account-card__counters'>
 | 
			
		||||
            <div className='account-card__counters__item'>
 | 
			
		||||
              <ShortNumber value={account.get('statuses_count')} />
 | 
			
		||||
              <small>
 | 
			
		||||
                <FormattedMessage id='account.posts' defaultMessage='Posts' />
 | 
			
		||||
              </small>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className='account-card__counters__item'>
 | 
			
		||||
              <ShortNumber value={account.get('followers_count')} />{' '}
 | 
			
		||||
              <small>
 | 
			
		||||
                <FormattedMessage
 | 
			
		||||
                  id='account.followers'
 | 
			
		||||
                  defaultMessage='Followers'
 | 
			
		||||
                />
 | 
			
		||||
              </small>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className='account-card__counters__item'>
 | 
			
		||||
              <ShortNumber value={account.get('following_count')} />{' '}
 | 
			
		||||
              <small>
 | 
			
		||||
                <FormattedMessage
 | 
			
		||||
                  id='account.following'
 | 
			
		||||
                  defaultMessage='Following'
 | 
			
		||||
                />
 | 
			
		||||
              </small>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account-card__actions__button'>
 | 
			
		||||
            {actionBtn}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(AccountCard));
 | 
			
		||||
@ -0,0 +1,269 @@
 | 
			
		||||
import type { MouseEventHandler } from 'react';
 | 
			
		||||
import { useCallback } from 'react';
 | 
			
		||||
 | 
			
		||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import classNames from 'classnames';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  followAccount,
 | 
			
		||||
  unfollowAccount,
 | 
			
		||||
  unblockAccount,
 | 
			
		||||
  unmuteAccount,
 | 
			
		||||
} from 'mastodon/actions/accounts';
 | 
			
		||||
import { openModal } from 'mastodon/actions/modal';
 | 
			
		||||
import { Avatar } from 'mastodon/components/avatar';
 | 
			
		||||
import { Button } from 'mastodon/components/button';
 | 
			
		||||
import { DisplayName } from 'mastodon/components/display_name';
 | 
			
		||||
import { ShortNumber } from 'mastodon/components/short_number';
 | 
			
		||||
import { autoPlayGif, me } from 'mastodon/initial_state';
 | 
			
		||||
import type { Account } from 'mastodon/models/account';
 | 
			
		||||
import { makeGetAccount } from 'mastodon/selectors';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
const messages = defineMessages({
 | 
			
		||||
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
 | 
			
		||||
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
 | 
			
		||||
  cancel_follow_request: {
 | 
			
		||||
    id: 'account.cancel_follow_request',
 | 
			
		||||
    defaultMessage: 'Withdraw follow request',
 | 
			
		||||
  },
 | 
			
		||||
  cancelFollowRequestConfirm: {
 | 
			
		||||
    id: 'confirmations.cancel_follow_request.confirm',
 | 
			
		||||
    defaultMessage: 'Withdraw request',
 | 
			
		||||
  },
 | 
			
		||||
  requested: {
 | 
			
		||||
    id: 'account.requested',
 | 
			
		||||
    defaultMessage: 'Awaiting approval. Click to cancel follow request',
 | 
			
		||||
  },
 | 
			
		||||
  unblock: { id: 'account.unblock_short', defaultMessage: 'Unblock' },
 | 
			
		||||
  unmute: { id: 'account.unmute_short', defaultMessage: 'Unmute' },
 | 
			
		||||
  unfollowConfirm: {
 | 
			
		||||
    id: 'confirmations.unfollow.confirm',
 | 
			
		||||
    defaultMessage: 'Unfollow',
 | 
			
		||||
  },
 | 
			
		||||
  edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getAccount = makeGetAccount();
 | 
			
		||||
 | 
			
		||||
export const AccountCard: React.FC<{ accountId: string }> = ({ accountId }) => {
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const account = useAppSelector((s) => getAccount(s, accountId));
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
  const handleMouseEnter = useCallback<MouseEventHandler>(
 | 
			
		||||
    ({ currentTarget }) => {
 | 
			
		||||
      if (autoPlayGif) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const emojis =
 | 
			
		||||
        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
 | 
			
		||||
 | 
			
		||||
      emojis.forEach((emoji) => {
 | 
			
		||||
        const original = emoji.getAttribute('data-original');
 | 
			
		||||
        if (original) emoji.src = original;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleMouseLeave = useCallback<MouseEventHandler>(
 | 
			
		||||
    ({ currentTarget }) => {
 | 
			
		||||
      if (autoPlayGif) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const emojis =
 | 
			
		||||
        currentTarget.querySelectorAll<HTMLImageElement>('.custom-emoji');
 | 
			
		||||
 | 
			
		||||
      emojis.forEach((emoji) => {
 | 
			
		||||
        const staticUrl = emoji.getAttribute('data-static');
 | 
			
		||||
        if (staticUrl) emoji.src = staticUrl;
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
    [],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleFollow = useCallback(() => {
 | 
			
		||||
    if (!account) return;
 | 
			
		||||
 | 
			
		||||
    if (account.getIn(['relationship', 'following'])) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        openModal({
 | 
			
		||||
          modalType: 'CONFIRM',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            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 if (account.getIn(['relationship', 'requested'])) {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        openModal({
 | 
			
		||||
          modalType: 'CONFIRM',
 | 
			
		||||
          modalProps: {
 | 
			
		||||
            message: (
 | 
			
		||||
              <FormattedMessage
 | 
			
		||||
                id='confirmations.cancel_follow_request.message'
 | 
			
		||||
                defaultMessage='Are you sure you want to withdraw your request to follow {name}?'
 | 
			
		||||
                values={{ name: <strong>@{account.get('acct')}</strong> }}
 | 
			
		||||
              />
 | 
			
		||||
            ),
 | 
			
		||||
            confirm: intl.formatMessage(messages.cancelFollowRequestConfirm),
 | 
			
		||||
            onConfirm: () => {
 | 
			
		||||
              dispatch(unfollowAccount(account.get('id')));
 | 
			
		||||
            },
 | 
			
		||||
          },
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(followAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  }, [account, dispatch, intl]);
 | 
			
		||||
 | 
			
		||||
  const handleBlock = useCallback(() => {
 | 
			
		||||
    if (account?.relationship?.blocking) {
 | 
			
		||||
      dispatch(unblockAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  }, [account, dispatch]);
 | 
			
		||||
 | 
			
		||||
  const handleMute = useCallback(() => {
 | 
			
		||||
    if (account?.relationship?.muting) {
 | 
			
		||||
      dispatch(unmuteAccount(account.get('id')));
 | 
			
		||||
    }
 | 
			
		||||
  }, [account, dispatch]);
 | 
			
		||||
 | 
			
		||||
  const handleEditProfile = useCallback(() => {
 | 
			
		||||
    window.open('/settings/profile', '_blank');
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  if (!account) return null;
 | 
			
		||||
 | 
			
		||||
  let actionBtn;
 | 
			
		||||
 | 
			
		||||
  if (me !== account.get('id')) {
 | 
			
		||||
    if (!account.get('relationship')) {
 | 
			
		||||
      // Wait until the relationship is loaded
 | 
			
		||||
      actionBtn = '';
 | 
			
		||||
    } else if (account.getIn(['relationship', 'requested'])) {
 | 
			
		||||
      actionBtn = (
 | 
			
		||||
        <Button
 | 
			
		||||
          text={intl.formatMessage(messages.cancel_follow_request)}
 | 
			
		||||
          title={intl.formatMessage(messages.requested)}
 | 
			
		||||
          onClick={handleFollow}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (account.getIn(['relationship', 'muting'])) {
 | 
			
		||||
      actionBtn = (
 | 
			
		||||
        <Button
 | 
			
		||||
          text={intl.formatMessage(messages.unmute)}
 | 
			
		||||
          onClick={handleMute}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (!account.getIn(['relationship', 'blocking'])) {
 | 
			
		||||
      actionBtn = (
 | 
			
		||||
        <Button
 | 
			
		||||
          disabled={account.relationship?.blocked_by}
 | 
			
		||||
          className={classNames({
 | 
			
		||||
            'button--destructive': account.getIn(['relationship', 'following']),
 | 
			
		||||
          })}
 | 
			
		||||
          text={intl.formatMessage(
 | 
			
		||||
            account.getIn(['relationship', 'following'])
 | 
			
		||||
              ? messages.unfollow
 | 
			
		||||
              : messages.follow,
 | 
			
		||||
          )}
 | 
			
		||||
          onClick={handleFollow}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    } else if (account.getIn(['relationship', 'blocking'])) {
 | 
			
		||||
      actionBtn = (
 | 
			
		||||
        <Button
 | 
			
		||||
          text={intl.formatMessage(messages.unblock)}
 | 
			
		||||
          onClick={handleBlock}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    actionBtn = (
 | 
			
		||||
      <Button
 | 
			
		||||
        text={intl.formatMessage(messages.edit_profile)}
 | 
			
		||||
        onClick={handleEditProfile}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className='account-card'>
 | 
			
		||||
      <Link to={`/@${account.get('acct')}`} className='account-card__permalink'>
 | 
			
		||||
        <div className='account-card__header'>
 | 
			
		||||
          <img
 | 
			
		||||
            src={
 | 
			
		||||
              autoPlayGif ? account.get('header') : account.get('header_static')
 | 
			
		||||
            }
 | 
			
		||||
            alt=''
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='account-card__title'>
 | 
			
		||||
          <div className='account-card__title__avatar'>
 | 
			
		||||
            <Avatar account={account as Account} size={56} />
 | 
			
		||||
          </div>
 | 
			
		||||
          <DisplayName account={account as Account} />
 | 
			
		||||
        </div>
 | 
			
		||||
      </Link>
 | 
			
		||||
 | 
			
		||||
      {account.get('note').length > 0 && (
 | 
			
		||||
        <div
 | 
			
		||||
          className='account-card__bio translate'
 | 
			
		||||
          onMouseEnter={handleMouseEnter}
 | 
			
		||||
          onMouseLeave={handleMouseLeave}
 | 
			
		||||
          dangerouslySetInnerHTML={{ __html: account.get('note_emojified') }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <div className='account-card__actions'>
 | 
			
		||||
        <div className='account-card__counters'>
 | 
			
		||||
          <div className='account-card__counters__item'>
 | 
			
		||||
            <ShortNumber value={account.get('statuses_count')} />
 | 
			
		||||
            <small>
 | 
			
		||||
              <FormattedMessage id='account.posts' defaultMessage='Posts' />
 | 
			
		||||
            </small>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account-card__counters__item'>
 | 
			
		||||
            <ShortNumber value={account.get('followers_count')} />{' '}
 | 
			
		||||
            <small>
 | 
			
		||||
              <FormattedMessage
 | 
			
		||||
                id='account.followers'
 | 
			
		||||
                defaultMessage='Followers'
 | 
			
		||||
              />
 | 
			
		||||
            </small>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div className='account-card__counters__item'>
 | 
			
		||||
            <ShortNumber value={account.get('following_count')} />{' '}
 | 
			
		||||
            <small>
 | 
			
		||||
              <FormattedMessage
 | 
			
		||||
                id='account.following'
 | 
			
		||||
                defaultMessage='Following'
 | 
			
		||||
              />
 | 
			
		||||
            </small>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='account-card__actions__button'>{actionBtn}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@ -1,181 +0,0 @@
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { PureComponent } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, injectIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { Helmet } from 'react-helmet';
 | 
			
		||||
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
import ImmutablePropTypes from 'react-immutable-proptypes';
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
 | 
			
		||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
 | 
			
		||||
import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
 | 
			
		||||
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 | 
			
		||||
import Column from 'mastodon/components/column';
 | 
			
		||||
import ColumnHeader from 'mastodon/components/column_header';
 | 
			
		||||
import { LoadMore } from 'mastodon/components/load_more';
 | 
			
		||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
			
		||||
import { RadioButton } from 'mastodon/components/radio_button';
 | 
			
		||||
import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
			
		||||
 | 
			
		||||
import AccountCard from './components/account_card';
 | 
			
		||||
 | 
			
		||||
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']),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
class Directory extends PureComponent {
 | 
			
		||||
 | 
			
		||||
  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'>
 | 
			
		||||
        <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='directory__list'>
 | 
			
		||||
          {isLoading ? <LoadingIndicator /> : 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'
 | 
			
		||||
          iconComponent={PeopleIcon}
 | 
			
		||||
          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}
 | 
			
		||||
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>{intl.formatMessage(messages.title)}</title>
 | 
			
		||||
          <meta name='robots' content='noindex' />
 | 
			
		||||
        </Helmet>
 | 
			
		||||
      </Column>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(mapStateToProps)(injectIntl(Directory));
 | 
			
		||||
							
								
								
									
										217
									
								
								app/javascript/mastodon/features/directory/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										217
									
								
								app/javascript/mastodon/features/directory/index.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,217 @@
 | 
			
		||||
import type { ChangeEventHandler } from 'react';
 | 
			
		||||
import { useCallback, useEffect, useRef, useState } from 'react';
 | 
			
		||||
 | 
			
		||||
import { defineMessages, useIntl } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import { Helmet } from 'react-helmet';
 | 
			
		||||
 | 
			
		||||
import { List as ImmutableList } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
 | 
			
		||||
import {
 | 
			
		||||
  addColumn,
 | 
			
		||||
  removeColumn,
 | 
			
		||||
  moveColumn,
 | 
			
		||||
  changeColumnParams,
 | 
			
		||||
} from 'mastodon/actions/columns';
 | 
			
		||||
import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
 | 
			
		||||
import Column from 'mastodon/components/column';
 | 
			
		||||
import ColumnHeader from 'mastodon/components/column_header';
 | 
			
		||||
import { LoadMore } from 'mastodon/components/load_more';
 | 
			
		||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
 | 
			
		||||
import { RadioButton } from 'mastodon/components/radio_button';
 | 
			
		||||
import ScrollContainer from 'mastodon/containers/scroll_container';
 | 
			
		||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
 | 
			
		||||
 | 
			
		||||
import { AccountCard } from './components/account_card';
 | 
			
		||||
 | 
			
		||||
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',
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const Directory: React.FC<{
 | 
			
		||||
  columnId?: string;
 | 
			
		||||
  multiColumn?: boolean;
 | 
			
		||||
  params?: { order: string; local?: boolean };
 | 
			
		||||
}> = ({ columnId, multiColumn, params }) => {
 | 
			
		||||
  const intl = useIntl();
 | 
			
		||||
  const dispatch = useAppDispatch();
 | 
			
		||||
 | 
			
		||||
  const [state, setState] = useState<{
 | 
			
		||||
    order: string | null;
 | 
			
		||||
    local: boolean | null;
 | 
			
		||||
  }>({
 | 
			
		||||
    order: null,
 | 
			
		||||
    local: null,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const column = useRef<Column>(null);
 | 
			
		||||
 | 
			
		||||
  const order = state.order ?? params?.order ?? 'active';
 | 
			
		||||
  const local = state.local ?? params?.local ?? false;
 | 
			
		||||
 | 
			
		||||
  const handlePin = useCallback(() => {
 | 
			
		||||
    if (columnId) {
 | 
			
		||||
      dispatch(removeColumn(columnId));
 | 
			
		||||
    } else {
 | 
			
		||||
      dispatch(addColumn('DIRECTORY', { order, local }));
 | 
			
		||||
    }
 | 
			
		||||
  }, [dispatch, columnId, order, local]);
 | 
			
		||||
 | 
			
		||||
  const domain = useAppSelector((s) => s.meta.get('domain') as string);
 | 
			
		||||
  const accountIds = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      state.user_lists.getIn(
 | 
			
		||||
        ['directory', 'items'],
 | 
			
		||||
        ImmutableList(),
 | 
			
		||||
      ) as ImmutableList<string>,
 | 
			
		||||
  );
 | 
			
		||||
  const isLoading = useAppSelector(
 | 
			
		||||
    (state) =>
 | 
			
		||||
      state.user_lists.getIn(['directory', 'isLoading'], true) as boolean,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    void dispatch(fetchDirectory({ order, local }));
 | 
			
		||||
  }, [dispatch, order, local]);
 | 
			
		||||
 | 
			
		||||
  const handleMove = useCallback(
 | 
			
		||||
    (dir: string) => {
 | 
			
		||||
      dispatch(moveColumn(columnId, dir));
 | 
			
		||||
    },
 | 
			
		||||
    [dispatch, columnId],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleHeaderClick = useCallback(() => {
 | 
			
		||||
    column.current?.scrollTop();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const handleChangeOrder = useCallback<ChangeEventHandler<HTMLInputElement>>(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      if (columnId) {
 | 
			
		||||
        dispatch(changeColumnParams(columnId, ['order'], e.target.value));
 | 
			
		||||
      } else {
 | 
			
		||||
        setState((s) => ({ order: e.target.value, local: s.local }));
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [dispatch, columnId],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleChangeLocal = useCallback<ChangeEventHandler<HTMLInputElement>>(
 | 
			
		||||
    (e) => {
 | 
			
		||||
      if (columnId) {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          changeColumnParams(columnId, ['local'], e.target.value === '1'),
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        setState((s) => ({ local: e.target.value === '1', order: s.order }));
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    [dispatch, columnId],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handleLoadMore = useCallback(() => {
 | 
			
		||||
    void dispatch(expandDirectory({ order, local }));
 | 
			
		||||
  }, [dispatch, order, local]);
 | 
			
		||||
 | 
			
		||||
  const pinned = !!columnId;
 | 
			
		||||
 | 
			
		||||
  const scrollableArea = (
 | 
			
		||||
    <div className='scrollable'>
 | 
			
		||||
      <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={handleChangeOrder}
 | 
			
		||||
          />
 | 
			
		||||
          <RadioButton
 | 
			
		||||
            name='order'
 | 
			
		||||
            value='new'
 | 
			
		||||
            label={intl.formatMessage(messages.newArrivals)}
 | 
			
		||||
            checked={order === 'new'}
 | 
			
		||||
            onChange={handleChangeOrder}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div className='filter-form__column' role='group'>
 | 
			
		||||
          <RadioButton
 | 
			
		||||
            name='local'
 | 
			
		||||
            value='1'
 | 
			
		||||
            label={intl.formatMessage(messages.local, { domain })}
 | 
			
		||||
            checked={local}
 | 
			
		||||
            onChange={handleChangeLocal}
 | 
			
		||||
          />
 | 
			
		||||
          <RadioButton
 | 
			
		||||
            name='local'
 | 
			
		||||
            value='0'
 | 
			
		||||
            label={intl.formatMessage(messages.federated)}
 | 
			
		||||
            checked={!local}
 | 
			
		||||
            onChange={handleChangeLocal}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div className='directory__list'>
 | 
			
		||||
        {isLoading ? (
 | 
			
		||||
          <LoadingIndicator />
 | 
			
		||||
        ) : (
 | 
			
		||||
          accountIds.map((accountId) => (
 | 
			
		||||
            <AccountCard accountId={accountId} key={accountId} />
 | 
			
		||||
          ))
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <LoadMore onClick={handleLoadMore} visible={!isLoading} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Column
 | 
			
		||||
      bindToDocument={!multiColumn}
 | 
			
		||||
      ref={column}
 | 
			
		||||
      label={intl.formatMessage(messages.title)}
 | 
			
		||||
    >
 | 
			
		||||
      <ColumnHeader
 | 
			
		||||
        // @ts-expect-error ColumnHeader is not properly typed yet
 | 
			
		||||
        icon='address-book-o'
 | 
			
		||||
        iconComponent={PeopleIcon}
 | 
			
		||||
        title={intl.formatMessage(messages.title)}
 | 
			
		||||
        onPin={handlePin}
 | 
			
		||||
        onMove={handleMove}
 | 
			
		||||
        onClick={handleHeaderClick}
 | 
			
		||||
        pinned={pinned}
 | 
			
		||||
        multiColumn={multiColumn}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      {multiColumn && !pinned ? (
 | 
			
		||||
        // @ts-expect-error ScrollContainer is not properly typed yet
 | 
			
		||||
        <ScrollContainer scrollKey='directory'>
 | 
			
		||||
          {scrollableArea}
 | 
			
		||||
        </ScrollContainer>
 | 
			
		||||
      ) : (
 | 
			
		||||
        scrollableArea
 | 
			
		||||
      )}
 | 
			
		||||
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>{intl.formatMessage(messages.title)}</title>
 | 
			
		||||
        <meta name='robots' content='noindex' />
 | 
			
		||||
      </Helmet>
 | 
			
		||||
    </Column>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line import/no-default-export -- Needed because this is called as an async components
 | 
			
		||||
export default Directory;
 | 
			
		||||
@ -1,12 +1,8 @@
 | 
			
		||||
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  DIRECTORY_FETCH_REQUEST,
 | 
			
		||||
  DIRECTORY_FETCH_SUCCESS,
 | 
			
		||||
  DIRECTORY_FETCH_FAIL,
 | 
			
		||||
  DIRECTORY_EXPAND_REQUEST,
 | 
			
		||||
  DIRECTORY_EXPAND_SUCCESS,
 | 
			
		||||
  DIRECTORY_EXPAND_FAIL,
 | 
			
		||||
  expandDirectory,
 | 
			
		||||
  fetchDirectory
 | 
			
		||||
} from 'mastodon/actions/directory';
 | 
			
		||||
import {
 | 
			
		||||
  FEATURED_TAGS_FETCH_REQUEST,
 | 
			
		||||
@ -117,6 +113,7 @@ const normalizeFeaturedTags = (state, path, featuredTags, accountId) => {
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** @type {import('@reduxjs/toolkit').Reducer<typeof initialState>} */
 | 
			
		||||
export default function userLists(state = initialState, action) {
 | 
			
		||||
  switch(action.type) {
 | 
			
		||||
  case FOLLOWERS_FETCH_SUCCESS:
 | 
			
		||||
@ -194,16 +191,6 @@ export default function userLists(state = initialState, action) {
 | 
			
		||||
  case MUTES_FETCH_FAIL:
 | 
			
		||||
  case MUTES_EXPAND_FAIL:
 | 
			
		||||
    return state.setIn(['mutes', 'isLoading'], false);
 | 
			
		||||
  case DIRECTORY_FETCH_SUCCESS:
 | 
			
		||||
    return normalizeList(state, ['directory'], action.accounts, action.next);
 | 
			
		||||
  case DIRECTORY_EXPAND_SUCCESS:
 | 
			
		||||
    return appendToList(state, ['directory'], action.accounts, action.next);
 | 
			
		||||
  case DIRECTORY_FETCH_REQUEST:
 | 
			
		||||
  case DIRECTORY_EXPAND_REQUEST:
 | 
			
		||||
    return state.setIn(['directory', 'isLoading'], true);
 | 
			
		||||
  case DIRECTORY_FETCH_FAIL:
 | 
			
		||||
  case DIRECTORY_EXPAND_FAIL:
 | 
			
		||||
    return state.setIn(['directory', 'isLoading'], false);
 | 
			
		||||
  case FEATURED_TAGS_FETCH_SUCCESS:
 | 
			
		||||
    return normalizeFeaturedTags(state, ['featured_tags', action.id], action.tags, action.id);
 | 
			
		||||
  case FEATURED_TAGS_FETCH_REQUEST:
 | 
			
		||||
@ -211,6 +198,17 @@ export default function userLists(state = initialState, action) {
 | 
			
		||||
  case FEATURED_TAGS_FETCH_FAIL:
 | 
			
		||||
    return state.setIn(['featured_tags', action.id, 'isLoading'], false);
 | 
			
		||||
  default:
 | 
			
		||||
    return state;
 | 
			
		||||
    if(fetchDirectory.fulfilled.match(action))
 | 
			
		||||
      return normalizeList(state, ['directory'], action.payload.accounts, undefined);
 | 
			
		||||
    else if( expandDirectory.fulfilled.match(action))
 | 
			
		||||
      return appendToList(state, ['directory'], action.payload.accounts, undefined);
 | 
			
		||||
    else if(fetchDirectory.pending.match(action) ||
 | 
			
		||||
     expandDirectory.pending.match(action))
 | 
			
		||||
      return state.setIn(['directory', 'isLoading'], true);
 | 
			
		||||
    else if(fetchDirectory.rejected.match(action) ||
 | 
			
		||||
     expandDirectory.rejected.match(action))
 | 
			
		||||
      return state.setIn(['directory', 'isLoading'], false);
 | 
			
		||||
    else
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user