mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Add share dialog for collections (#37986)
This commit is contained in:
@@ -7,6 +7,8 @@ import { useHovering } from 'mastodon/hooks/useHovering';
|
||||
import { autoPlayGif } from 'mastodon/initial_state';
|
||||
import type { Account } from 'mastodon/models/account';
|
||||
|
||||
import { useAccount } from '../hooks/useAccount';
|
||||
|
||||
interface Props {
|
||||
account:
|
||||
| Pick<Account, 'id' | 'acct' | 'avatar' | 'avatar_static'>
|
||||
@@ -91,3 +93,10 @@ export const Avatar: React.FC<Props> = ({
|
||||
|
||||
return avatar;
|
||||
};
|
||||
|
||||
export const AvatarById: React.FC<
|
||||
{ accountId: string } & Omit<Props, 'account'>
|
||||
> = ({ accountId, ...otherProps }) => {
|
||||
const account = useAccount(accountId);
|
||||
return <Avatar account={account} {...otherProps} />;
|
||||
};
|
||||
|
||||
@@ -19,8 +19,9 @@ const messages = defineMessages({
|
||||
export const CopyIconButton: React.FC<{
|
||||
title: string;
|
||||
value: string;
|
||||
className: string;
|
||||
}> = ({ title, value, className }) => {
|
||||
className?: string;
|
||||
'aria-describedby'?: string;
|
||||
}> = ({ title, value, className, 'aria-describedby': ariaDescribedBy }) => {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@@ -38,8 +39,9 @@ export const CopyIconButton: React.FC<{
|
||||
className={classNames(className, copied ? 'copied' : 'copyable')}
|
||||
title={title}
|
||||
onClick={handleClick}
|
||||
icon=''
|
||||
icon='copy-icon'
|
||||
iconComponent={ContentCopyIcon}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input {
|
||||
padding-inline-end: 45px;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
position: absolute;
|
||||
inset-inline-end: 0;
|
||||
top: 0;
|
||||
padding: 9px;
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { CopyIconButton } from 'mastodon/components/copy_icon_button';
|
||||
|
||||
import classes from './copy_link_field.module.scss';
|
||||
import { FormFieldWrapper } from './form_field_wrapper';
|
||||
import type { CommonFieldWrapperProps } from './form_field_wrapper';
|
||||
import { TextInput } from './text_input_field';
|
||||
import type { TextInputProps } from './text_input_field';
|
||||
|
||||
interface CopyLinkFieldProps extends CommonFieldWrapperProps, TextInputProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A read-only text field with a button for copying the field value
|
||||
*/
|
||||
|
||||
export const CopyLinkField = forwardRef<HTMLInputElement, CopyLinkFieldProps>(
|
||||
(
|
||||
{ id, label, hint, hasError, value, required, className, ...otherProps },
|
||||
ref,
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const inputRef = useRef<HTMLInputElement | null>();
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current?.select();
|
||||
}, []);
|
||||
|
||||
const mergeRefs = useCallback(
|
||||
(element: HTMLInputElement | null) => {
|
||||
inputRef.current = element;
|
||||
if (typeof ref === 'function') {
|
||||
ref(element);
|
||||
} else if (ref) {
|
||||
ref.current = element;
|
||||
}
|
||||
},
|
||||
[ref],
|
||||
);
|
||||
|
||||
return (
|
||||
<FormFieldWrapper
|
||||
label={label}
|
||||
hint={hint}
|
||||
required={required}
|
||||
hasError={hasError}
|
||||
inputId={id}
|
||||
>
|
||||
{(inputProps) => (
|
||||
<div className={classes.wrapper}>
|
||||
<TextInput
|
||||
readOnly
|
||||
{...otherProps}
|
||||
{...inputProps}
|
||||
ref={mergeRefs}
|
||||
value={value}
|
||||
onFocus={handleFocus}
|
||||
className={classNames(className, classes.input)}
|
||||
/>
|
||||
<CopyIconButton
|
||||
value={value}
|
||||
title={intl.formatMessage({
|
||||
id: 'copy_icon_button.copy_this_text',
|
||||
defaultMessage: 'Copy link to clipboard',
|
||||
})}
|
||||
className={classes.copyButton}
|
||||
aria-describedby={inputProps.id}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</FormFieldWrapper>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
CopyLinkField.displayName = 'CopyLinkField';
|
||||
@@ -1,3 +1,4 @@
|
||||
export { FormFieldWrapper } from './form_field_wrapper';
|
||||
export { FormStack } from './form_stack';
|
||||
export { Fieldset } from './fieldset';
|
||||
export { TextInputField, TextInput } from './text_input_field';
|
||||
@@ -8,6 +9,7 @@ export {
|
||||
Combobox,
|
||||
type ComboboxItemState,
|
||||
} from './combobox_field';
|
||||
export { CopyLinkField } from './copy_link_field';
|
||||
export { RadioButtonField, RadioButton } from './radio_button_field';
|
||||
export { ToggleField, Toggle } from './toggle_field';
|
||||
export { SelectField, Select } from './select_field';
|
||||
|
||||
56
app/javascript/mastodon/components/modal_shell/index.tsx
Normal file
56
app/javascript/mastodon/components/modal_shell/index.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface SimpleComponentProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ModalShellComponent extends React.FC<SimpleComponentProps> {
|
||||
Body: React.FC<SimpleComponentProps>;
|
||||
Actions: React.FC<SimpleComponentProps>;
|
||||
}
|
||||
|
||||
export const ModalShell: ModalShellComponent = ({ children, className }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'modal-root__modal',
|
||||
'safety-action-modal',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalShellBody: ModalShellComponent['Body'] = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className='safety-action-modal__top'>
|
||||
<div
|
||||
className={classNames('safety-action-modal__confirmation', className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalShellActions: ModalShellComponent['Actions'] = ({
|
||||
children,
|
||||
className,
|
||||
}) => {
|
||||
return (
|
||||
<div className='safety-action-modal__bottom'>
|
||||
<div className={classNames('safety-action-modal__actions', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModalShell.Body = ModalShellBody;
|
||||
ModalShell.Actions = ModalShellActions;
|
||||
@@ -44,6 +44,7 @@
|
||||
--gap: 0.75ch;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap);
|
||||
|
||||
& > li:not(:last-child)::after {
|
||||
|
||||
@@ -3,18 +3,21 @@ import { useCallback, useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useParams } from 'react-router';
|
||||
import { useLocation, useParams } from 'react-router';
|
||||
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { useRelationship } from '@/mastodon/hooks/useRelationship';
|
||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
import { showAlert } from 'mastodon/actions/alerts';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { Account } from 'mastodon/components/account';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { Column } from 'mastodon/components/column';
|
||||
import { ColumnHeader } from 'mastodon/components/column_header';
|
||||
import { LinkedDisplayName } from 'mastodon/components/display_name';
|
||||
import {
|
||||
DisplayName,
|
||||
LinkedDisplayName,
|
||||
} from 'mastodon/components/display_name';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import ScrollableList from 'mastodon/components/scrollable_list';
|
||||
import { Tag } from 'mastodon/components/tags/tag';
|
||||
@@ -46,32 +49,40 @@ const messages = defineMessages({
|
||||
},
|
||||
});
|
||||
|
||||
const AuthorNote: React.FC<{ id: string }> = ({ id }) => {
|
||||
export const AuthorNote: React.FC<{ id: string; previewMode?: boolean }> = ({
|
||||
id,
|
||||
// When previewMode is enabled, your own display name
|
||||
// will not be replaced with "you"
|
||||
previewMode = false,
|
||||
}) => {
|
||||
const account = useAccount(id);
|
||||
const author = (
|
||||
<span className={classes.displayNameWithAvatar}>
|
||||
<Avatar size={18} account={account} />
|
||||
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
|
||||
{previewMode ? (
|
||||
<DisplayName account={account} variant='simple' />
|
||||
) : (
|
||||
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (id === me) {
|
||||
return (
|
||||
<p className={classes.authorNote}>
|
||||
const displayAsYou = id === me && !previewMode;
|
||||
|
||||
return (
|
||||
<p className={previewMode ? classes.previewAuthorNote : classes.authorNote}>
|
||||
{displayAsYou ? (
|
||||
<FormattedMessage
|
||||
id='collections.detail.curated_by_you'
|
||||
defaultMessage='Curated by you'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p className={classes.authorNote}>
|
||||
<FormattedMessage
|
||||
id='collections.detail.curated_by_author'
|
||||
defaultMessage='Curated by {author}'
|
||||
values={{ author }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collections.detail.curated_by_author'
|
||||
defaultMessage='Curated by {author}'
|
||||
values={{ author }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
@@ -84,8 +95,23 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
dispatch(showAlert({ message: 'Collection sharing not yet implemented' }));
|
||||
}, [dispatch]);
|
||||
dispatch(
|
||||
openModal({
|
||||
modalType: 'SHARE_COLLECTION',
|
||||
modalProps: {
|
||||
collection,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [collection, dispatch]);
|
||||
|
||||
const location = useLocation<{ newCollection?: boolean }>();
|
||||
const wasJustCreated = location.state.newCollection;
|
||||
useEffect(() => {
|
||||
if (wasJustCreated) {
|
||||
handleShare();
|
||||
}
|
||||
}, [handleShare, wasJustCreated]);
|
||||
|
||||
return (
|
||||
<div className={classes.header}>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
.heading {
|
||||
font-size: 28px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-wrap: wrap-reverse;
|
||||
align-items: start;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
color: var(--color-text-primary);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--color-bg-brand-soft),
|
||||
var(--color-bg-primary)
|
||||
);
|
||||
border: 1px solid var(--color-bg-brand-base);
|
||||
}
|
||||
|
||||
.previewHeading {
|
||||
font-size: 22px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
$bottomsheet-breakpoint: 630px;
|
||||
|
||||
.shareButtonWrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
|
||||
& > button {
|
||||
flex: 1;
|
||||
min-width: 220px;
|
||||
white-space: normal;
|
||||
|
||||
@media (width > $bottomsheet-breakpoint) {
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.closeButtonDesktop {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
inset-inline-end: 4px;
|
||||
padding: 8px;
|
||||
|
||||
@media (width <= $bottomsheet-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButtonMobile {
|
||||
margin-top: 16px;
|
||||
margin-bottom: -18px;
|
||||
|
||||
@media (width > $bottomsheet-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { changeCompose, focusCompose } from 'mastodon/actions/compose';
|
||||
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
|
||||
import { AvatarById } from 'mastodon/components/avatar';
|
||||
import { AvatarGroup } from 'mastodon/components/avatar_group';
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { CopyLinkField } from 'mastodon/components/form_fields';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { ModalShell } from 'mastodon/components/modal_shell';
|
||||
import { useAppDispatch } from 'mastodon/store';
|
||||
|
||||
import { AuthorNote } from '.';
|
||||
import classes from './share_modal.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
shareTextOwn: {
|
||||
id: 'collection.share_template_own',
|
||||
defaultMessage: 'Check out my new collection: {link}',
|
||||
},
|
||||
shareTextOther: {
|
||||
id: 'collection.share_template_other',
|
||||
defaultMessage: 'Check out this cool collection: {link}',
|
||||
},
|
||||
});
|
||||
|
||||
export const CollectionShareModal: React.FC<{
|
||||
collection: ApiCollectionJSON;
|
||||
onClose: () => void;
|
||||
}> = ({ collection, onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const location = useLocation<{ newCollection?: boolean }>();
|
||||
const isNew = !!location.state.newCollection;
|
||||
const isOwnCollection = collection.account_id === me;
|
||||
|
||||
const collectionLink = `${window.location.origin}/collections/${collection.id}`;
|
||||
|
||||
const handleShareOnDevice = useCallback(() => {
|
||||
void navigator.share({
|
||||
url: collectionLink,
|
||||
});
|
||||
}, [collectionLink]);
|
||||
|
||||
const handleShareViaPost = useCallback(() => {
|
||||
const shareMessage = isOwnCollection
|
||||
? intl.formatMessage(messages.shareTextOwn, {
|
||||
link: collectionLink,
|
||||
})
|
||||
: intl.formatMessage(messages.shareTextOther, {
|
||||
link: collectionLink,
|
||||
});
|
||||
|
||||
onClose();
|
||||
dispatch(changeCompose(shareMessage));
|
||||
dispatch(focusCompose());
|
||||
}, [collectionLink, dispatch, intl, isOwnCollection, onClose]);
|
||||
|
||||
return (
|
||||
<ModalShell>
|
||||
<ModalShell.Body>
|
||||
<h1 className={classes.heading}>
|
||||
{isNew ? (
|
||||
<FormattedMessage
|
||||
id='collection.share_modal.title_new'
|
||||
defaultMessage='Share your new collection!'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='collection.share_modal.title'
|
||||
defaultMessage='Share collection'
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
|
||||
<IconButton
|
||||
title={intl.formatMessage({
|
||||
id: 'lightbox.close',
|
||||
defaultMessage: 'Close',
|
||||
})}
|
||||
iconComponent={CloseIcon}
|
||||
icon='close'
|
||||
className={classes.closeButtonDesktop}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
<div className={classes.preview}>
|
||||
<div>
|
||||
<h2 className={classes.previewHeading}>{collection.name}</h2>
|
||||
<AuthorNote previewMode id={collection.account_id} />
|
||||
</div>
|
||||
<AvatarGroup>
|
||||
{collection.items.slice(0, 5).map(({ account_id }) => {
|
||||
if (!account_id) return;
|
||||
return (
|
||||
<AvatarById key={account_id} accountId={account_id} size={28} />
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
|
||||
<CopyLinkField
|
||||
label={intl.formatMessage({
|
||||
id: 'collection.share_modal.share_link_label',
|
||||
defaultMessage: 'Invite share link',
|
||||
})}
|
||||
value={collectionLink}
|
||||
/>
|
||||
</ModalShell.Body>
|
||||
|
||||
<ModalShell.Actions className={classes.actions}>
|
||||
<div className={classes.shareButtonWrapper}>
|
||||
<Button secondary onClick={handleShareViaPost}>
|
||||
<FormattedMessage
|
||||
id='collection.share_modal.share_via_post'
|
||||
defaultMessage='Post on Mastodon'
|
||||
/>
|
||||
</Button>
|
||||
{'share' in navigator && (
|
||||
<Button secondary onClick={handleShareOnDevice}>
|
||||
<FormattedMessage
|
||||
id='collection.share_modal.share_via_system'
|
||||
defaultMessage='Share to…'
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button plain onClick={onClose} className={classes.closeButtonMobile}>
|
||||
<FormattedMessage id='lightbox.close' defaultMessage='Close' />
|
||||
</Button>
|
||||
</ModalShell.Actions>
|
||||
</ModalShell>
|
||||
);
|
||||
};
|
||||
@@ -48,6 +48,10 @@
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.previewAuthorNote {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.metaData {
|
||||
margin-top: 16px;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -127,7 +127,9 @@ export const CollectionDetails: React.FC<{
|
||||
history.replace(
|
||||
`/collections/${result.payload.collection.id}/edit/details`,
|
||||
);
|
||||
history.push(`/collections/${result.payload.collection.id}`);
|
||||
history.push(`/collections/${result.payload.collection.id}`, {
|
||||
newCollection: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useCallback } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button } from 'mastodon/components/button';
|
||||
import { ModalShell } from 'mastodon/components/modal_shell';
|
||||
|
||||
export interface BaseConfirmationModalProps {
|
||||
onClose: () => void;
|
||||
@@ -56,53 +57,49 @@ export const ConfirmationModal: React.FC<
|
||||
}, [onClose, onSecondary]);
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal safety-action-modal'>
|
||||
<div className='safety-action-modal__top'>
|
||||
<div className='safety-action-modal__confirmation'>
|
||||
<h1 id={titleId}>{title}</h1>
|
||||
{message && <p>{message}</p>}
|
||||
<ModalShell>
|
||||
<ModalShell.Body>
|
||||
<h1 id={titleId}>{title}</h1>
|
||||
{message && <p>{message}</p>}
|
||||
|
||||
{extraContent ?? children}
|
||||
</div>
|
||||
</div>
|
||||
{extraContent ?? children}
|
||||
</ModalShell.Body>
|
||||
|
||||
<div className='safety-action-modal__bottom'>
|
||||
<div className='safety-action-modal__actions'>
|
||||
<button onClick={onClose} className='link-button' type='button'>
|
||||
{cancel ?? (
|
||||
<FormattedMessage
|
||||
id='confirmation_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{secondary && (
|
||||
<>
|
||||
<div className='spacer' />
|
||||
<button
|
||||
onClick={handleSecondary}
|
||||
className='link-button'
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
>
|
||||
{secondary}
|
||||
</button>
|
||||
</>
|
||||
<ModalShell.Actions>
|
||||
<button onClick={onClose} className='link-button' type='button'>
|
||||
{cancel ?? (
|
||||
<FormattedMessage
|
||||
id='confirmation_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
loading={updating}
|
||||
disabled={disabled}
|
||||
autoFocus={!noFocusButton}
|
||||
>
|
||||
{confirm}
|
||||
</Button>
|
||||
{/* eslint-enable */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{secondary && (
|
||||
<>
|
||||
<div className='spacer' />
|
||||
<button
|
||||
onClick={handleSecondary}
|
||||
className='link-button'
|
||||
type='button'
|
||||
disabled={disabled}
|
||||
>
|
||||
{secondary}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
loading={updating}
|
||||
disabled={disabled}
|
||||
autoFocus={!noFocusButton}
|
||||
>
|
||||
{confirm}
|
||||
</Button>
|
||||
{/* eslint-enable */}
|
||||
</ModalShell.Actions>
|
||||
</ModalShell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
DomainBlockModal,
|
||||
ReportModal,
|
||||
ReportCollectionModal,
|
||||
ShareCollectionModal,
|
||||
EmbedModal,
|
||||
ListAdder,
|
||||
CompareHistoryModal,
|
||||
@@ -79,6 +80,7 @@ export const MODAL_COMPONENTS = {
|
||||
'DOMAIN_BLOCK': DomainBlockModal,
|
||||
'REPORT': ReportModal,
|
||||
'REPORT_COLLECTION': ReportCollectionModal,
|
||||
'SHARE_COLLECTION': ShareCollectionModal,
|
||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||
'EMBED': EmbedModal,
|
||||
'FOCAL_POINT': () => Promise.resolve({ default: AltTextModal }),
|
||||
|
||||
@@ -62,6 +62,12 @@ export function CollectionsEditor() {
|
||||
);
|
||||
}
|
||||
|
||||
export function ShareCollectionModal() {
|
||||
return import('../../collections/detail/share_modal').then(
|
||||
module => ({default: module.CollectionShareModal})
|
||||
);
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import('../../status');
|
||||
}
|
||||
|
||||
@@ -271,6 +271,13 @@
|
||||
"closed_registrations_modal.find_another_server": "Find another server",
|
||||
"closed_registrations_modal.preamble": "Mastodon is decentralized, so no matter where you create your account, you will be able to follow and interact with anyone on this server. You can even self-host it!",
|
||||
"closed_registrations_modal.title": "Signing up on Mastodon",
|
||||
"collection.share_modal.share_link_label": "Invite share link",
|
||||
"collection.share_modal.share_via_post": "Post on Mastodon",
|
||||
"collection.share_modal.share_via_system": "Share to…",
|
||||
"collection.share_modal.title": "Share collection",
|
||||
"collection.share_modal.title_new": "Share your new collection!",
|
||||
"collection.share_template_other": "Check out this cool collection: {link}",
|
||||
"collection.share_template_own": "Check out my new collection: {link}",
|
||||
"collections.account_count": "{count, plural, one {# account} other {# accounts}}",
|
||||
"collections.accounts.empty_description": "Add up to {count} accounts you follow",
|
||||
"collections.accounts.empty_title": "This collection is empty",
|
||||
@@ -448,6 +455,7 @@
|
||||
"conversation.open": "View conversation",
|
||||
"conversation.with": "With {names}",
|
||||
"copy_icon_button.copied": "Copied to clipboard",
|
||||
"copy_icon_button.copy_this_text": "Copy link to clipboard",
|
||||
"copypaste.copied": "Copied",
|
||||
"copypaste.copy_to_clipboard": "Copy to clipboard",
|
||||
"directory.federated": "From known fediverse",
|
||||
|
||||
@@ -6408,15 +6408,15 @@ a.status-card {
|
||||
line-height: 20px;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
h1 {
|
||||
:where(h1) {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:not(:only-child) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
:where(h1:not(:only-child)) {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
strong {
|
||||
|
||||
Reference in New Issue
Block a user