[Glitch] Add share dialog for collections
Port 7970eb392abea1a452bccfc04b673fc55e117578 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
0e1ea3110e
commit
9ed23cc8b2
@ -7,6 +7,8 @@ import { useHovering } from 'flavours/glitch/hooks/useHovering';
|
|||||||
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
import { autoPlayGif } from 'flavours/glitch/initial_state';
|
||||||
import type { Account } from 'flavours/glitch/models/account';
|
import type { Account } from 'flavours/glitch/models/account';
|
||||||
|
|
||||||
|
import { useAccount } from '../hooks/useAccount';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
account:
|
account:
|
||||||
| Pick<Account, 'id' | 'acct' | 'avatar' | 'avatar_static'>
|
| Pick<Account, 'id' | 'acct' | 'avatar' | 'avatar_static'>
|
||||||
@ -92,3 +94,10 @@ export const Avatar: React.FC<Props> = ({
|
|||||||
|
|
||||||
return avatar;
|
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<{
|
export const CopyIconButton: React.FC<{
|
||||||
title: string;
|
title: string;
|
||||||
value: string;
|
value: string;
|
||||||
className: string;
|
className?: string;
|
||||||
}> = ({ title, value, className }) => {
|
'aria-describedby'?: string;
|
||||||
|
}> = ({ title, value, className, 'aria-describedby': ariaDescribedBy }) => {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
@ -38,8 +39,9 @@ export const CopyIconButton: React.FC<{
|
|||||||
className={classNames(className, copied ? 'copied' : 'copyable')}
|
className={classNames(className, copied ? 'copied' : 'copyable')}
|
||||||
title={title}
|
title={title}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
icon=''
|
icon='copy-icon'
|
||||||
iconComponent={ContentCopyIcon}
|
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 'flavours/glitch/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 { FormStack } from './form_stack';
|
||||||
export { Fieldset } from './fieldset';
|
export { Fieldset } from './fieldset';
|
||||||
export { TextInputField, TextInput } from './text_input_field';
|
export { TextInputField, TextInput } from './text_input_field';
|
||||||
@ -8,6 +9,7 @@ export {
|
|||||||
Combobox,
|
Combobox,
|
||||||
type ComboboxItemState,
|
type ComboboxItemState,
|
||||||
} from './combobox_field';
|
} from './combobox_field';
|
||||||
|
export { CopyLinkField } from './copy_link_field';
|
||||||
export { RadioButtonField, RadioButton } from './radio_button_field';
|
export { RadioButtonField, RadioButton } from './radio_button_field';
|
||||||
export { ToggleField, Toggle } from './toggle_field';
|
export { ToggleField, Toggle } from './toggle_field';
|
||||||
export { SelectField, Select } from './select_field';
|
export { SelectField, Select } from './select_field';
|
||||||
|
|||||||
@ -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;
|
--gap: 0.75ch;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: var(--gap);
|
gap: var(--gap);
|
||||||
|
|
||||||
& > li:not(:last-child)::after {
|
& > li:not(:last-child)::after {
|
||||||
|
|||||||
@ -3,18 +3,21 @@ import { useCallback, useEffect } from 'react';
|
|||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useParams } from 'react-router';
|
import { useLocation, useParams } from 'react-router';
|
||||||
|
|
||||||
|
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||||
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
|
import { useRelationship } from '@/flavours/glitch/hooks/useRelationship';
|
||||||
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
import ListAltIcon from '@/material-icons/400-24px/list_alt.svg?react';
|
||||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||||
import { showAlert } from 'flavours/glitch/actions/alerts';
|
|
||||||
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
import { Account } from 'flavours/glitch/components/account';
|
import { Account } from 'flavours/glitch/components/account';
|
||||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||||
import { Column } from 'flavours/glitch/components/column';
|
import { Column } from 'flavours/glitch/components/column';
|
||||||
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
import { ColumnHeader } from 'flavours/glitch/components/column_header';
|
||||||
import { LinkedDisplayName } from 'flavours/glitch/components/display_name';
|
import {
|
||||||
|
DisplayName,
|
||||||
|
LinkedDisplayName,
|
||||||
|
} from 'flavours/glitch/components/display_name';
|
||||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||||
import { Tag } from 'flavours/glitch/components/tags/tag';
|
import { Tag } from 'flavours/glitch/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 account = useAccount(id);
|
||||||
const author = (
|
const author = (
|
||||||
<span className={classes.displayNameWithAvatar}>
|
<span className={classes.displayNameWithAvatar}>
|
||||||
<Avatar size={18} account={account} />
|
<Avatar size={18} account={account} />
|
||||||
|
{previewMode ? (
|
||||||
|
<DisplayName account={account} variant='simple' />
|
||||||
|
) : (
|
||||||
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
|
<LinkedDisplayName displayProps={{ account, variant: 'simple' }} />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (id === me) {
|
const displayAsYou = id === me && !previewMode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className={classes.authorNote}>
|
<p className={previewMode ? classes.previewAuthorNote : classes.authorNote}>
|
||||||
|
{displayAsYou ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='collections.detail.curated_by_you'
|
id='collections.detail.curated_by_you'
|
||||||
defaultMessage='Curated by you'
|
defaultMessage='Curated by you'
|
||||||
/>
|
/>
|
||||||
</p>
|
) : (
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<p className={classes.authorNote}>
|
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='collections.detail.curated_by_author'
|
id='collections.detail.curated_by_author'
|
||||||
defaultMessage='Curated by {author}'
|
defaultMessage='Curated by {author}'
|
||||||
values={{ author }}
|
values={{ author }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -84,8 +95,23 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
|
|||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const handleShare = useCallback(() => {
|
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 (
|
return (
|
||||||
<div className={classes.header}>
|
<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 '@/flavours/glitch/initial_state';
|
||||||
|
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||||
|
import { changeCompose, focusCompose } from 'flavours/glitch/actions/compose';
|
||||||
|
import type { ApiCollectionJSON } from 'flavours/glitch/api_types/collections';
|
||||||
|
import { AvatarById } from 'flavours/glitch/components/avatar';
|
||||||
|
import { AvatarGroup } from 'flavours/glitch/components/avatar_group';
|
||||||
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
import { CopyLinkField } from 'flavours/glitch/components/form_fields';
|
||||||
|
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||||
|
import { ModalShell } from 'flavours/glitch/components/modal_shell';
|
||||||
|
import { useAppDispatch } from 'flavours/glitch/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);
|
color: var(--color-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.previewAuthorNote {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.metaData {
|
.metaData {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
@ -127,7 +127,9 @@ export const CollectionDetails: React.FC<{
|
|||||||
history.replace(
|
history.replace(
|
||||||
`/collections/${result.payload.collection.id}/edit/details`,
|
`/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, defineMessages } from 'react-intl';
|
import { FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
import { Button } from 'flavours/glitch/components/button';
|
import { Button } from 'flavours/glitch/components/button';
|
||||||
|
import { ModalShell } from 'flavours/glitch/components/modal_shell';
|
||||||
|
|
||||||
export interface BaseConfirmationModalProps {
|
export interface BaseConfirmationModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -64,18 +65,15 @@ export const ConfirmationModal: React.FC<
|
|||||||
}, [onClose, onSecondary]);
|
}, [onClose, onSecondary]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='modal-root__modal safety-action-modal'>
|
<ModalShell>
|
||||||
<div className='safety-action-modal__top'>
|
<ModalShell.Body>
|
||||||
<div className='safety-action-modal__confirmation'>
|
|
||||||
<h1 id={titleId}>{title}</h1>
|
<h1 id={titleId}>{title}</h1>
|
||||||
{message && <p>{message}</p>}
|
{message && <p>{message}</p>}
|
||||||
|
|
||||||
{extraContent ?? children}
|
{extraContent ?? children}
|
||||||
</div>
|
</ModalShell.Body>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='safety-action-modal__bottom'>
|
<ModalShell.Actions>
|
||||||
<div className='safety-action-modal__actions'>
|
|
||||||
<button onClick={onClose} className='link-button' type='button'>
|
<button onClick={onClose} className='link-button' type='button'>
|
||||||
{cancel ?? (
|
{cancel ?? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -109,8 +107,7 @@ export const ConfirmationModal: React.FC<
|
|||||||
{confirm}
|
{confirm}
|
||||||
</Button>
|
</Button>
|
||||||
{/* eslint-enable */}
|
{/* eslint-enable */}
|
||||||
</div>
|
</ModalShell.Actions>
|
||||||
</div>
|
</ModalShell>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
DomainBlockModal,
|
DomainBlockModal,
|
||||||
ReportModal,
|
ReportModal,
|
||||||
ReportCollectionModal,
|
ReportCollectionModal,
|
||||||
|
ShareCollectionModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
EmbedModal,
|
EmbedModal,
|
||||||
ListAdder,
|
ListAdder,
|
||||||
@ -85,6 +86,7 @@ export const MODAL_COMPONENTS = {
|
|||||||
'DOMAIN_BLOCK': DomainBlockModal,
|
'DOMAIN_BLOCK': DomainBlockModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
'REPORT_COLLECTION': ReportCollectionModal,
|
'REPORT_COLLECTION': ReportCollectionModal,
|
||||||
|
'SHARE_COLLECTION': ShareCollectionModal,
|
||||||
'SETTINGS': SettingsModal,
|
'SETTINGS': SettingsModal,
|
||||||
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
'DEPRECATED_SETTINGS': () => Promise.resolve({ default: DeprecatedSettingsModal }),
|
||||||
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
'ACTIONS': () => Promise.resolve({ default: ActionsModal }),
|
||||||
|
|||||||
@ -62,6 +62,12 @@ export function CollectionsEditor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ShareCollectionModal() {
|
||||||
|
return import('../../collections/detail/share_modal').then(
|
||||||
|
module => ({default: module.CollectionShareModal})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function Status () {
|
export function Status () {
|
||||||
return import('../../status');
|
return import('../../status');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6652,15 +6652,15 @@ a.status-card {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|
||||||
h1 {
|
:where(h1) {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&:not(:only-child) {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:where(h1:not(:only-child)) {
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
strong {
|
strong {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user