220 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
| 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 'flavours/glitch/actions/columns';
 | |
| import {
 | |
|   fetchDirectory,
 | |
|   expandDirectory,
 | |
| } from 'flavours/glitch/actions/directory';
 | |
| import Column from 'flavours/glitch/components/column';
 | |
| import { ColumnHeader } from 'flavours/glitch/components/column_header';
 | |
| import { LoadMore } from 'flavours/glitch/components/load_more';
 | |
| import { LoadingIndicator } from 'flavours/glitch/components/loading_indicator';
 | |
| import { RadioButton } from 'flavours/glitch/components/radio_button';
 | |
| import ScrollContainer from 'flavours/glitch/containers/scroll_container';
 | |
| import { useAppDispatch, useAppSelector } from 'flavours/glitch/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: number) => {
 | |
|       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
 | |
|         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;
 |