Merge branch 'links-a11y' into 'develop'

Prefer accessible links

See merge request soapbox-pub/soapbox!2648
environments/review-develop-3zknud/deployments/3742
marcin mikołajczak 1 year ago
commit 631c94f771

@ -1,5 +1,6 @@
import clsx from 'clsx'; import clsx from 'clsx';
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { SelectDropdown } from '../features/forms'; import { SelectDropdown } from '../features/forms';
@ -17,13 +18,14 @@ const List: React.FC<IList> = ({ children }) => (
interface IListItem { interface IListItem {
label: React.ReactNode label: React.ReactNode
hint?: React.ReactNode hint?: React.ReactNode
to?: string
onClick?(): void onClick?(): void
onSelect?(): void onSelect?(): void
isSelected?: boolean isSelected?: boolean
children?: React.ReactNode children?: React.ReactNode
} }
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelect, isSelected }) => { const ListItem: React.FC<IListItem> = ({ label, hint, children, to, onClick, onSelect, isSelected }) => {
const id = uuidv4(); const id = uuidv4();
const domId = `list-group-${id}`; const domId = `list-group-${id}`;
@ -33,9 +35,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
} }
}; };
const Comp = onClick ? 'a' : 'div'; const LabelComp = to || onClick || onSelect ? 'span' : 'label';
const LabelComp = onClick || onSelect ? 'span' : 'label';
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {};
const renderChildren = React.useCallback(() => { const renderChildren = React.useCallback(() => {
return React.Children.map(children, (child) => { return React.Children.map(children, (child) => {
@ -55,13 +55,12 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
}); });
}, [children, domId]); }, [children, domId]);
return ( const className = clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', {
<Comp 'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof to !== 'undefined' || typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
className={clsx('flex items-center justify-between overflow-hidden bg-gradient-to-r from-gradient-start/20 to-gradient-end/20 px-4 py-2 first:rounded-t-lg last:rounded-b-lg dark:from-gradient-start/10 dark:to-gradient-end/10', { });
'cursor-pointer hover:from-gradient-start/30 hover:to-gradient-end/30 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
})} const body = (
{...linkProps} <>
>
<div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'> <div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'>
<LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp> <LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
@ -70,7 +69,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
) : null} ) : null}
</div> </div>
{onClick ? ( {(to || onClick) ? (
<HStack space={1} alignItems='center' className='overflow-hidden text-gray-700 dark:text-gray-600'> <HStack space={1} alignItems='center' className='overflow-hidden text-gray-700 dark:text-gray-600'>
{children} {children}
@ -105,8 +104,24 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
</div> </div>
) : null} ) : null}
{typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null} {typeof to === 'undefined' && typeof onClick === 'undefined' && typeof onSelect === 'undefined' ? renderChildren() : null}
</Comp> </>
);
if (to) return (
<Link className={className} to={to}>
{body}
</Link>
);
const Comp = onClick ? 'a' : 'div';
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {};
return (
<Comp
className={className}
{...linkProps}
/>
); );
}; };

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list'; import { getSubscribersCsv, getUnsubscribersCsv, getCombinedCsv } from 'soapbox/actions/email-list';
import List, { ListItem } from 'soapbox/components/list'; import List, { ListItem } from 'soapbox/components/list';
@ -15,7 +14,6 @@ import RegistrationModePicker from '../components/registration-mode-picker';
const Dashboard: React.FC = () => { const Dashboard: React.FC = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory();
const instance = useInstance(); const instance = useInstance();
const features = useFeatures(); const features = useFeatures();
const { account } = useOwnAccount(); const { account } = useOwnAccount();
@ -41,10 +39,6 @@ const Dashboard: React.FC = () => {
e.preventDefault(); e.preventDefault();
}; };
const navigateToSoapboxConfig = () => history.push('/soapbox/config');
const navigateToModerationLog = () => history.push('/soapbox/admin/log');
const navigateToAnnouncements = () => history.push('/soapbox/admin/announcements');
const v = parseVersion(instance.version); const v = parseVersion(instance.version);
const userCount = instance.stats.get('user_count'); const userCount = instance.stats.get('user_count');
@ -87,19 +81,19 @@ const Dashboard: React.FC = () => {
<List> <List>
{account.admin && ( {account.admin && (
<ListItem <ListItem
onClick={navigateToSoapboxConfig} to='/soapbox/config'
label={<FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' />} label={<FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' />}
/> />
)} )}
<ListItem <ListItem
onClick={navigateToModerationLog} to='/soapbox/admin/log'
label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />} label={<FormattedMessage id='column.admin.moderation_log' defaultMessage='Moderation Log' />}
/> />
{features.announcements && ( {features.announcements && (
<ListItem <ListItem
onClick={navigateToAnnouncements} to='/soapbox/admin/announcements'
label={<FormattedMessage id='column.admin.announcements' defaultMessage='Announcements' />} label={<FormattedMessage id='column.admin.announcements' defaultMessage='Announcements' />}
/> />
)} )}

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { changeEmail } from 'soapbox/actions/security'; import { changeEmail } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch } from 'soapbox/hooks'; import { useAppDispatch } from 'soapbox/hooks';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
@ -48,47 +48,33 @@ const EditEmail = () => {
}, [email, password, dispatch, intl]); }, [email, password, dispatch, intl]);
return ( return (
<Column <Column label={intl.formatMessage(messages.header)} backHref='/settings'>
label={intl.formatMessage(messages.header)} <Form onSubmit={handleSubmit}>
transparent <FormGroup labelText={intl.formatMessage(messages.emailFieldLabel)}>
withHeader={false} <Input
> type='text'
<Card variant='rounded'> placeholder={intl.formatMessage(messages.emailFieldPlaceholder)}
<CardHeader backHref='/settings'> name='email'
<CardTitle autoComplete='off'
title={intl.formatMessage(messages.header)} onChange={handleInputChange}
value={email}
/> />
</CardHeader> </FormGroup>
<CardBody> <FormGroup labelText={intl.formatMessage(messages.passwordFieldLabel)}>
<Form onSubmit={handleSubmit}> <Input
<FormGroup labelText={intl.formatMessage(messages.emailFieldLabel)}> type='password'
<Input name='password'
type='text' onChange={handleInputChange}
placeholder={intl.formatMessage(messages.emailFieldPlaceholder)} value={password}
name='email' />
autoComplete='off' </FormGroup>
onChange={handleInputChange}
value={email}
/>
</FormGroup>
<FormGroup labelText={intl.formatMessage(messages.passwordFieldLabel)}>
<Input
type='password'
name='password'
onChange={handleInputChange}
value={password}
/>
</FormGroup>
<FormActions> <FormActions>
<Button to='/settings' theme='tertiary'>{intl.formatMessage(messages.cancel)}</Button> <Button to='/settings' theme='tertiary'>{intl.formatMessage(messages.cancel)}</Button>
<Button type='submit' theme='primary' disabled={isLoading}>{intl.formatMessage(messages.submit)}</Button> <Button type='submit' theme='primary' disabled={isLoading}>{intl.formatMessage(messages.submit)}</Button>
</FormActions> </FormActions>
</Form> </Form>
</CardBody>
</Card>
</Column> </Column>
); );
}; };

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { changePassword } from 'soapbox/actions/security'; import { changePassword } from 'soapbox/actions/security';
import { Button, Card, CardBody, CardHeader, CardTitle, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui'; import { Button, Column, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import toast from 'soapbox/toast'; import toast from 'soapbox/toast';
@ -55,57 +55,49 @@ const EditPassword = () => {
}, [currentPassword, newPassword, newPasswordConfirmation, dispatch, intl]); }, [currentPassword, newPassword, newPasswordConfirmation, dispatch, intl]);
return ( return (
<Column label={intl.formatMessage(messages.header)} transparent withHeader={false}> <Column label={intl.formatMessage(messages.header)} backHref='/settings'>
<Card variant='rounded'> <Form onSubmit={handleSubmit}>
<CardHeader backHref='/settings'> <FormGroup labelText={intl.formatMessage(messages.oldPasswordFieldLabel)}>
<CardTitle title={intl.formatMessage(messages.header)} /> <Input
</CardHeader> type='password'
name='currentPassword'
<CardBody> onChange={handleInputChange}
<Form onSubmit={handleSubmit}> value={currentPassword}
<FormGroup labelText={intl.formatMessage(messages.oldPasswordFieldLabel)}> />
<Input </FormGroup>
type='password'
name='currentPassword' <FormGroup labelText={intl.formatMessage(messages.newPasswordFieldLabel)}>
onChange={handleInputChange} <Input
value={currentPassword} type='password'
/> name='newPassword'
</FormGroup> onChange={handleInputChange}
value={newPassword}
<FormGroup labelText={intl.formatMessage(messages.newPasswordFieldLabel)}> />
<Input
type='password' {passwordRequirements && (
name='newPassword' <PasswordIndicator password={newPassword} onChange={setHasValidPassword} />
onChange={handleInputChange} )}
value={newPassword} </FormGroup>
/>
<FormGroup labelText={intl.formatMessage(messages.confirmationFieldLabel)}>
{passwordRequirements && ( <Input
<PasswordIndicator password={newPassword} onChange={setHasValidPassword} /> type='password'
)} name='newPasswordConfirmation'
</FormGroup> onChange={handleInputChange}
value={newPasswordConfirmation}
<FormGroup labelText={intl.formatMessage(messages.confirmationFieldLabel)}> />
<Input </FormGroup>
type='password'
name='newPasswordConfirmation' <FormActions>
onChange={handleInputChange} <Button to='/settings' theme='tertiary'>
value={newPasswordConfirmation} {intl.formatMessage(messages.cancel)}
/> </Button>
</FormGroup>
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
<FormActions> {intl.formatMessage(messages.submit)}
<Button to='/settings' theme='tertiary'> </Button>
{intl.formatMessage(messages.cancel)} </FormActions>
</Button> </Form>
<Button type='submit' theme='primary' disabled={isLoading || !hasValidPassword}>
{intl.formatMessage(messages.submit)}
</Button>
</FormActions>
</Form>
</CardBody>
</Card>
</Column> </Column>
); );
}; };

@ -76,10 +76,6 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
}, },
})); }));
const navigateToEdit = () => history.push(`/group/${group.slug}/manage/edit`);
const navigateToPending = () => history.push(`/group/${group.slug}/manage/requests`);
const navigateToBlocks = () => history.push(`/group/${group.slug}/manage/blocks`);
return ( return (
<Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.slug}`}> <Column label={intl.formatMessage(messages.heading)} backHref={`/group/${group.slug}`}>
<CardBody className='space-y-4'> <CardBody className='space-y-4'>
@ -90,7 +86,7 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
</CardHeader> </CardHeader>
<List> <List>
<ListItem label={intl.formatMessage(messages.editGroup)} onClick={navigateToEdit}> <ListItem label={intl.formatMessage(messages.editGroup)} to={`/group/${group.slug}/manage/edit`}>
<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} /> <span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
</ListItem> </ListItem>
</List> </List>
@ -103,10 +99,10 @@ const ManageGroup: React.FC<IManageGroup> = ({ params }) => {
<List> <List>
{backend.software !== TRUTHSOCIAL && ( {backend.software !== TRUTHSOCIAL && (
<ListItem label={intl.formatMessage(messages.pendingRequests)} onClick={navigateToPending} /> <ListItem label={intl.formatMessage(messages.pendingRequests)} to={`/group/${group.slug}/manage/requests`} />
)} )}
<ListItem label={intl.formatMessage(messages.blockedMembers)} onClick={navigateToBlocks} /> <ListItem label={intl.formatMessage(messages.blockedMembers)} to={`/group/${group.slug}/manage/blocks`} />
</List> </List>
{isOwner && ( {isOwner && (

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { useHistory } from 'react-router-dom';
import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui'; import { Avatar, Button, CardTitle, Stack } from 'soapbox/components/ui';
import { type Card as StatusCard } from 'soapbox/types/entities'; import { type Card as StatusCard } from 'soapbox/types/entities';
@ -9,13 +8,9 @@ interface IGroupLinkPreview {
} }
const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => { const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
const history = useHistory();
const { group } = card; const { group } = card;
if (!group) return null; if (!group) return null;
const navigateToGroup = () => history.push(`/group/${group.slug}`);
return ( return (
<Stack className='cursor-default overflow-hidden rounded-lg border border-gray-300 text-center dark:border-gray-800'> <Stack className='cursor-default overflow-hidden rounded-lg border border-gray-300 text-center dark:border-gray-800'>
<div <div
@ -32,7 +27,7 @@ const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
<Stack space={4} className='p-4'> <Stack space={4} className='p-4'>
<CardTitle title={<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />} /> <CardTitle title={<span dangerouslySetInnerHTML={{ __html: group.display_name_html }} />} />
<Button theme='primary' onClick={navigateToGroup} block> <Button theme='primary' to={`/group/${group.slug}`} block>
View Group View Group
</Button> </Button>
</Stack> </Stack>
@ -40,4 +35,4 @@ const GroupLinkPreview: React.FC<IGroupLinkPreview> = ({ card }) => {
); );
}; };
export { GroupLinkPreview }; export { GroupLinkPreview };

@ -1,6 +1,5 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { fetchMfa } from 'soapbox/actions/mfa'; import { fetchMfa } from 'soapbox/actions/mfa';
import List, { ListItem } from 'soapbox/components/list'; import List, { ListItem } from 'soapbox/components/list';
@ -38,27 +37,12 @@ const messages = defineMessages({
/** User settings page. */ /** User settings page. */
const Settings = () => { const Settings = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const history = useHistory();
const intl = useIntl(); const intl = useIntl();
const mfa = useAppSelector((state) => state.security.get('mfa')); const mfa = useAppSelector((state) => state.security.get('mfa'));
const features = useFeatures(); const features = useFeatures();
const { account } = useOwnAccount(); const { account } = useOwnAccount();
const navigateToChangeEmail = () => history.push('/settings/email');
const navigateToChangePassword = () => history.push('/settings/password');
const navigateToMfa = () => history.push('/settings/mfa');
const navigateToSessions = () => history.push('/settings/tokens');
const navigateToEditProfile = () => history.push('/settings/profile');
const navigateToDeleteAccount = () => history.push('/settings/account');
const navigateToMoveAccount = () => history.push('/settings/migration');
const navigateToAliases = () => history.push('/settings/aliases');
const navigateToBackups = () => history.push('/settings/backups');
const navigateToImportData = () => history.push('/settings/import');
const navigateToExportData = () => history.push('/settings/export');
const navigateToMutes = () => history.push('/mutes');
const navigateToBlocks = () => history.push('/blocks');
const isMfaEnabled = mfa.getIn(['settings', 'totp']); const isMfaEnabled = mfa.getIn(['settings', 'totp']);
useEffect(() => { useEffect(() => {
@ -78,7 +62,7 @@ const Settings = () => {
<CardBody> <CardBody>
<List> <List>
<ListItem label={intl.formatMessage(messages.editProfile)} onClick={navigateToEditProfile}> <ListItem label={intl.formatMessage(messages.editProfile)} to='/settings/profile'>
<span className='max-w-full truncate'>{displayName}</span> <span className='max-w-full truncate'>{displayName}</span>
</ListItem> </ListItem>
</List> </List>
@ -90,8 +74,8 @@ const Settings = () => {
<CardBody> <CardBody>
<List> <List>
<ListItem label={intl.formatMessage(messages.mutes)} onClick={navigateToMutes} /> <ListItem label={intl.formatMessage(messages.mutes)} to='/mutes' />
<ListItem label={intl.formatMessage(messages.blocks)} onClick={navigateToBlocks} /> <ListItem label={intl.formatMessage(messages.blocks)} to='/blocks' />
</List> </List>
</CardBody> </CardBody>
@ -105,9 +89,9 @@ const Settings = () => {
<List> <List>
{features.security && ( {features.security && (
<> <>
<ListItem label={intl.formatMessage(messages.changeEmail)} onClick={navigateToChangeEmail} /> <ListItem label={intl.formatMessage(messages.changeEmail)} to='/settings/email' />
<ListItem label={intl.formatMessage(messages.changePassword)} onClick={navigateToChangePassword} /> <ListItem label={intl.formatMessage(messages.changePassword)} to='/settings/password' />
<ListItem label={intl.formatMessage(messages.configureMfa)} onClick={navigateToMfa}> <ListItem label={intl.formatMessage(messages.configureMfa)} to='/settings/mfa'>
<span> <span>
{isMfaEnabled ? {isMfaEnabled ?
intl.formatMessage(messages.mfaEnabled) : intl.formatMessage(messages.mfaEnabled) :
@ -117,7 +101,7 @@ const Settings = () => {
</> </>
)} )}
{features.sessions && ( {features.sessions && (
<ListItem label={intl.formatMessage(messages.sessions)} onClick={navigateToSessions} /> <ListItem label={intl.formatMessage(messages.sessions)} to='/settings/tokens' />
)} )}
</List> </List>
</CardBody> </CardBody>
@ -153,25 +137,25 @@ const Settings = () => {
<CardBody> <CardBody>
<List> <List>
{features.importData && ( {features.importData && (
<ListItem label={intl.formatMessage(messages.importData)} onClick={navigateToImportData} /> <ListItem label={intl.formatMessage(messages.importData)} to='/settings/import' />
)} )}
{features.exportData && ( {features.exportData && (
<ListItem label={intl.formatMessage(messages.exportData)} onClick={navigateToExportData} /> <ListItem label={intl.formatMessage(messages.exportData)} to='/settings/export' />
)} )}
{features.backups && ( {features.backups && (
<ListItem label={intl.formatMessage(messages.backups)} onClick={navigateToBackups} /> <ListItem label={intl.formatMessage(messages.backups)} to='/settings/backups' />
)} )}
{features.federating && (features.accountMoving ? ( {features.federating && (features.accountMoving ? (
<ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} /> <ListItem label={intl.formatMessage(messages.accountMigration)} to='/settings/migrations' />
) : features.accountAliases && ( ) : features.accountAliases && (
<ListItem label={intl.formatMessage(messages.accountAliases)} onClick={navigateToAliases} /> <ListItem label={intl.formatMessage(messages.accountAliases)} to='/settings/aliases' />
))} ))}
{features.security && ( {features.security && (
<ListItem label={<Text theme='danger'>{intl.formatMessage(messages.deleteAccount)}</Text>} onClick={navigateToDeleteAccount} /> <ListItem label={<Text theme='danger'>{intl.formatMessage(messages.deleteAccount)}</Text>} to='/settings/account' />
)} )}
</List> </List>
</CardBody> </CardBody>

@ -1,7 +1,6 @@
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import React, { useState, useEffect, useMemo } from 'react'; import React, { useState, useEffect, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { updateSoapboxConfig } from 'soapbox/actions/admin'; import { updateSoapboxConfig } from 'soapbox/actions/admin';
import { uploadMedia } from 'soapbox/actions/media'; import { uploadMedia } from 'soapbox/actions/media';
@ -70,7 +69,6 @@ const templates: Record<string, Template> = {
const SoapboxConfig: React.FC = () => { const SoapboxConfig: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const features = useFeatures(); const features = useFeatures();
@ -83,8 +81,6 @@ const SoapboxConfig: React.FC = () => {
const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2)); const [rawJSON, setRawJSON] = useState<string>(JSON.stringify(initialData, null, 2));
const [jsonValid, setJsonValid] = useState(true); const [jsonValid, setJsonValid] = useState(true);
const navigateToThemeEditor = () => history.push('/soapbox/admin/theme');
const soapbox = useMemo(() => { const soapbox = useMemo(() => {
return normalizeSoapboxConfig(data); return normalizeSoapboxConfig(data);
}, [data]); }, [data]);
@ -211,7 +207,7 @@ const SoapboxConfig: React.FC = () => {
<ListItem <ListItem
label={<FormattedMessage id='soapbox_config.fields.edit_theme_label' defaultMessage='Edit theme' />} label={<FormattedMessage id='soapbox_config.fields.edit_theme_label' defaultMessage='Edit theme' />}
onClick={navigateToThemeEditor} to='/soapbox/admin/theme'
/> />
</List> </List>

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { useHistory } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import { HStack, Text, Emoji } from 'soapbox/components/ui'; import { HStack, Text, Emoji } from 'soapbox/components/ui';
@ -17,8 +17,6 @@ interface IStatusInteractionBar {
} }
const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.Element | null => { const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.Element | null => {
const history = useHistory();
const me = useAppSelector(({ me }) => me); const me = useAppSelector(({ me }) => me);
const { allowedEmoji } = useSoapboxConfig(); const { allowedEmoji } = useSoapboxConfig();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -91,16 +89,10 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
return null; return null;
}; };
const navigateToQuotes: React.EventHandler<React.MouseEvent> = (e) => {
e.preventDefault();
history.push(`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`);
};
const getQuotes = () => { const getQuotes = () => {
if (status.quotes_count) { if (status.quotes_count) {
return ( return (
<InteractionCounter count={status.quotes_count} onClick={navigateToQuotes}> <InteractionCounter count={status.quotes_count} to={`/@${status.getIn(['account', 'acct'])}/posts/${status.id}/quotes`}>
<FormattedMessage <FormattedMessage
id='status.interactions.quotes' id='status.interactions.quotes'
defaultMessage='{count, plural, one {Quote} other {Quotes}}' defaultMessage='{count, plural, one {Quote} other {Quotes}}'
@ -209,34 +201,47 @@ const StatusInteractionBar: React.FC<IStatusInteractionBar> = ({ status }): JSX.
interface IInteractionCounter { interface IInteractionCounter {
count: number count: number
onClick?: React.MouseEventHandler<HTMLButtonElement>
children: React.ReactNode children: React.ReactNode
onClick?: React.MouseEventHandler<HTMLButtonElement>
to?: string
} }
const InteractionCounter: React.FC<IInteractionCounter> = ({ count, onClick, children }) => { const InteractionCounter: React.FC<IInteractionCounter> = ({ count, children, onClick, to }) => {
const features = useFeatures(); const features = useFeatures();
const className = clsx({
'text-gray-600 dark:text-gray-700': true,
'hover:underline': features.exposableReactions,
'cursor-default': !features.exposableReactions,
});
const body = (
<HStack space={1} alignItems='center'>
<Text weight='bold'>
{shortNumberFormat(count)}
</Text>
<Text tag='div' theme='muted'>
{children}
</Text>
</HStack>
);
if (to) {
return (
<Link to={to} className={className}>
{body}
</Link>
);
}
return ( return (
<button <button
type='button' type='button'
onClick={onClick} onClick={onClick}
className={ className={className}
clsx({
'text-gray-600 dark:text-gray-700': true,
'hover:underline': features.exposableReactions,
'cursor-default': !features.exposableReactions,
})
}
> >
<HStack space={1} alignItems='center'> {body}
<Text weight='bold'>
{shortNumberFormat(count)}
</Text>
<Text tag='div' theme='muted'>
{children}
</Text>
</HStack>
</button> </button>
); );
}; };

Loading…
Cancel
Save