mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Profile editing: Edit image menu (#38156)
Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
@@ -296,6 +296,7 @@ interface DropdownProps<Item extends object | null = MenuItem> {
|
||||
children?: React.ReactElement;
|
||||
icon?: string;
|
||||
iconComponent?: IconProp;
|
||||
iconClassName?: string;
|
||||
items?: Item[];
|
||||
loading?: boolean;
|
||||
title?: string;
|
||||
@@ -326,6 +327,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
children,
|
||||
icon,
|
||||
iconComponent,
|
||||
iconClassName,
|
||||
items,
|
||||
loading,
|
||||
title = 'Menu',
|
||||
@@ -499,6 +501,7 @@ export const Dropdown = <Item extends object | null = MenuItem>({
|
||||
iconComponent={iconComponent}
|
||||
title={title}
|
||||
active={open}
|
||||
className={iconClassName}
|
||||
{...buttonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import type { OffsetValue } from 'react-overlays/esm/usePopper';
|
||||
|
||||
import type { ModalType } from '@/mastodon/actions/modal';
|
||||
import { openModal } from '@/mastodon/actions/modal';
|
||||
import { Dropdown } from '@/mastodon/components/dropdown_menu';
|
||||
import { IconButton } from '@/mastodon/components/icon_button';
|
||||
import type { MenuItem } from '@/mastodon/models/dropdown_menu';
|
||||
import {
|
||||
createAppSelector,
|
||||
useAppDispatch,
|
||||
useAppSelector,
|
||||
} from '@/mastodon/store';
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import DeleteIcon from '@/material-icons/400-24px/delete.svg?react';
|
||||
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
import CameraIcon from '@/material-icons/400-24px/photo_camera.svg?react';
|
||||
import ReplaceImageIcon from '@/material-icons/400-24px/replace_image.svg?react';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
add: {
|
||||
id: 'account_edit.image_edit.add_button',
|
||||
defaultMessage: 'Add image',
|
||||
},
|
||||
replace: {
|
||||
id: 'account_edit.image_edit.replace_button',
|
||||
defaultMessage: 'Replace image',
|
||||
},
|
||||
altAdd: {
|
||||
id: 'account_edit.image_edit.alt_add_button',
|
||||
description: 'Alt is short for "alternative".',
|
||||
defaultMessage: 'Add alt text',
|
||||
},
|
||||
altEdit: {
|
||||
id: 'account_edit.image_edit.alt_edit_button',
|
||||
description: 'Alt is short for "alternative".',
|
||||
defaultMessage: 'Edit alt text',
|
||||
},
|
||||
remove: {
|
||||
id: 'account_edit.image_edit.remove_button',
|
||||
defaultMessage: 'Remove image',
|
||||
},
|
||||
});
|
||||
|
||||
export type ImageLocation = 'avatar' | 'header';
|
||||
|
||||
const selectImageInfo = createAppSelector(
|
||||
[
|
||||
(state) => state.profileEdit.profile,
|
||||
(_, location: ImageLocation) => location,
|
||||
],
|
||||
(profile, location) => {
|
||||
if (!profile) {
|
||||
return {
|
||||
hasImage: false,
|
||||
hasAlt: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasImage: !!profile[`${location}Static`],
|
||||
hasAlt: !!profile[`${location}Description`],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const AccountImageEdit: FC<{
|
||||
className?: string;
|
||||
location: ImageLocation;
|
||||
}> = ({ className, location }) => {
|
||||
const intl = useIntl();
|
||||
const { hasAlt, hasImage } = useAppSelector((state) =>
|
||||
selectImageInfo(state, location),
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleModal = useCallback(
|
||||
(type: ModalType) => {
|
||||
dispatch(openModal({ modalType: type, modalProps: { location } }));
|
||||
},
|
||||
[dispatch, location],
|
||||
);
|
||||
|
||||
const items = useMemo(
|
||||
() =>
|
||||
[
|
||||
{
|
||||
text: intl.formatMessage(messages.replace),
|
||||
action: () => {
|
||||
handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD');
|
||||
},
|
||||
icon: ReplaceImageIcon,
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage(hasAlt ? messages.altEdit : messages.altAdd),
|
||||
action: () => {
|
||||
handleModal('ACCOUNT_EDIT_IMAGE_ALT');
|
||||
},
|
||||
icon: hasAlt ? EditIcon : AddIcon,
|
||||
},
|
||||
null,
|
||||
{
|
||||
text: intl.formatMessage(messages.remove),
|
||||
action: () => {
|
||||
handleModal('ACCOUNT_EDIT_IMAGE_DELETE');
|
||||
},
|
||||
icon: DeleteIcon,
|
||||
dangerous: true,
|
||||
},
|
||||
] satisfies MenuItem[],
|
||||
[handleModal, hasAlt, intl],
|
||||
);
|
||||
|
||||
const handleAddImage = useCallback(() => {
|
||||
handleModal('ACCOUNT_EDIT_IMAGE_UPLOAD');
|
||||
}, [handleModal]);
|
||||
|
||||
const iconClassName = classNames(classes.imageButton, className);
|
||||
|
||||
if (!hasImage) {
|
||||
return (
|
||||
<IconButton
|
||||
title={intl.formatMessage(messages.add)}
|
||||
icon='camera'
|
||||
iconComponent={CameraIcon}
|
||||
className={iconClassName}
|
||||
onClick={handleAddImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={items}
|
||||
placement={location === 'header' ? 'bottom-end' : 'bottom-start'}
|
||||
offset={popperOffset}
|
||||
className={classes.imageMenu}
|
||||
icon='camera'
|
||||
title={intl.formatMessage(messages.replace)}
|
||||
iconComponent={CameraIcon}
|
||||
iconClassName={iconClassName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const popperOffset = [0, 6] as OffsetValue;
|
||||
@@ -23,6 +23,7 @@ import { AccountEditColumn, AccountEditEmptyColumn } from './components/column';
|
||||
import { EditButton } from './components/edit_button';
|
||||
import { AccountField } from './components/field';
|
||||
import { AccountFieldActions } from './components/field_actions';
|
||||
import { AccountImageEdit } from './components/image_edit';
|
||||
import { AccountEditSection } from './components/section';
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
@@ -164,8 +165,12 @@ export const AccountEdit: FC = () => {
|
||||
<header>
|
||||
<div className={classes.profileImage}>
|
||||
{headerSrc && <img src={headerSrc} alt='' />}
|
||||
<AccountImageEdit location='header' />
|
||||
</div>
|
||||
<div className={classes.avatar}>
|
||||
<Avatar account={account} size={80} />
|
||||
<AccountImageEdit location='avatar' />
|
||||
</div>
|
||||
<Avatar account={account} size={80} className={classes.avatar} />
|
||||
</header>
|
||||
|
||||
<CustomEmojiProvider emojis={emojis}>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||
import type { ImageLocation } from '../components/image_edit';
|
||||
|
||||
export const ImageAltModal: FC<
|
||||
DialogModalProps & { location: ImageLocation }
|
||||
> = ({ onClose }) => {
|
||||
return <DialogModal title='TODO' onClose={onClose} />;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||
import type { ImageLocation } from '../components/image_edit';
|
||||
|
||||
export const ImageDeleteModal: FC<
|
||||
DialogModalProps & { location: ImageLocation }
|
||||
> = ({ onClose }) => {
|
||||
return <DialogModal title='TODO' onClose={onClose} />;
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DialogModal } from '../../ui/components/dialog_modal';
|
||||
import type { DialogModalProps } from '../../ui/components/dialog_modal';
|
||||
import type { ImageLocation } from '../components/image_edit';
|
||||
|
||||
export const ImageUploadModal: FC<
|
||||
DialogModalProps & { location: ImageLocation }
|
||||
> = ({ onClose }) => {
|
||||
return <DialogModal title='TODO' onClose={onClose} />;
|
||||
};
|
||||
@@ -1,6 +1,9 @@
|
||||
export * from './bio_modal';
|
||||
export * from './fields_modals';
|
||||
export * from './fields_reorder_modal';
|
||||
export * from './image_alt';
|
||||
export * from './image_delete';
|
||||
export * from './image_upload';
|
||||
export * from './name_modal';
|
||||
export * from './profile_display_modal';
|
||||
export * from './verified_modal';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
background: var(--color-bg-secondary);
|
||||
border-bottom: 1px solid var(--color-border-primary);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
@container (width >= 500px) {
|
||||
height: 160px;
|
||||
@@ -16,12 +17,27 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.imageButton {
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-top: -64px;
|
||||
margin-left: 18px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
position: relative;
|
||||
width: 82px;
|
||||
|
||||
> :global(.account__avatar) {
|
||||
border: 1px solid var(--color-border-primary);
|
||||
}
|
||||
|
||||
.imageButton {
|
||||
bottom: -8px;
|
||||
right: -8px;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -153,6 +169,41 @@
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
// Image edit component
|
||||
|
||||
.imageButton {
|
||||
--default-bg-color: var(--color-bg-primary);
|
||||
|
||||
&,
|
||||
&:global(.active) {
|
||||
// Overrides the transparent background added by default with .active
|
||||
--hover-bg-color: var(--color-bg-brand-softer-solid);
|
||||
}
|
||||
|
||||
position: absolute;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--color-border-primary);
|
||||
border-radius: 9999px;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
transition:
|
||||
color 0.2s ease-in-out,
|
||||
background-color 0.2s ease-in-out;
|
||||
|
||||
svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.imageMenu {
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
// Item list component
|
||||
|
||||
.itemList {
|
||||
|
||||
@@ -293,11 +293,7 @@ svg.badgeIcon {
|
||||
|
||||
.fieldOverflowButton {
|
||||
--default-bg-color: var(--color-bg-secondary-solid);
|
||||
--hover-bg-color: color-mix(
|
||||
in oklab,
|
||||
var(--color-bg-brand-base),
|
||||
var(--default-bg-color) var(--overlay-strength-brand)
|
||||
);
|
||||
--hover-bg-color: var(--color-bg-brand-softer-solid);
|
||||
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
|
||||
@@ -102,6 +102,9 @@ export const MODAL_COMPONENTS = {
|
||||
'ACCOUNT_EDIT_FIELD_EDIT': accountEditModal('EditFieldModal'),
|
||||
'ACCOUNT_EDIT_FIELD_DELETE': accountEditModal('DeleteFieldModal'),
|
||||
'ACCOUNT_EDIT_FIELDS_REORDER': accountEditModal('ReorderFieldsModal'),
|
||||
'ACCOUNT_EDIT_IMAGE_ALT': accountEditModal('ImageAltModal'),
|
||||
'ACCOUNT_EDIT_IMAGE_DELETE': accountEditModal('ImageDeleteModal'),
|
||||
'ACCOUNT_EDIT_IMAGE_UPLOAD': accountEditModal('ImageUploadModal'),
|
||||
};
|
||||
|
||||
/** @arg {keyof import('@/mastodon/features/account_edit/modals')} type */
|
||||
|
||||
@@ -184,6 +184,11 @@
|
||||
"account_edit.field_reorder_modal.drag_start": "Picked up field \"{item}\".",
|
||||
"account_edit.field_reorder_modal.handle_label": "Drag field \"{item}\"",
|
||||
"account_edit.field_reorder_modal.title": "Rearrange fields",
|
||||
"account_edit.image_edit.add_button": "Add image",
|
||||
"account_edit.image_edit.alt_add_button": "Add alt text",
|
||||
"account_edit.image_edit.alt_edit_button": "Edit alt text",
|
||||
"account_edit.image_edit.remove_button": "Remove image",
|
||||
"account_edit.image_edit.replace_button": "Replace image",
|
||||
"account_edit.name_modal.add_title": "Add display name",
|
||||
"account_edit.name_modal.edit_title": "Edit display name",
|
||||
"account_edit.profile_tab.button_label": "Customize",
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M480-260q75 0 127.5-52.5T660-440q0-75-52.5-127.5T480-620q-75 0-127.5 52.5T300-440q0 75 52.5 127.5T480-260Zm0-80q-42 0-71-29t-29-71q0-42 29-71t71-29q42 0 71 29t29 71q0 42-29 71t-71 29ZM160-120q-33 0-56.5-23.5T80-200v-480q0-33 23.5-56.5T160-760h126l74-80h240l74 80h126q33 0 56.5 23.5T880-680v480q0 33-23.5 56.5T800-120H160Z"/></svg>
|
||||
|
After Width: | Height: | Size: 427 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-200h240l-79-103-58 69-39-52-64 86ZM320-80q-33 0-56.5-23.5T240-160v-320q0-33 23.5-56.5T320-560h320q33 0 56.5 23.5T720-480v320q0 33-23.5 56.5T640-80H320ZM140-640q38-109 131.5-174.5T480-880q82 0 155.5 35T760-746v-134h80v240H600v-80h76q-39-39-90-59.5T480-800q-81 0-149.5 43T227-640h-87Z"/></svg>
|
||||
|
After Width: | Height: | Size: 393 B |
1
app/javascript/material-icons/400-24px/replace_image.svg
Normal file
1
app/javascript/material-icons/400-24px/replace_image.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M360-200h240l-79-103-58 69-39-52-64 86ZM320-80q-33 0-56.5-23.5T240-160v-320q0-33 23.5-56.5T320-560h320q33 0 56.5 23.5T720-480v320q0 33-23.5 56.5T640-80H320Zm0-80h320v-320H320v320ZM140-640q38-109 131.5-174.5T480-880q82 0 155.5 35T760-746v-134h80v240H600v-80h76q-39-39-90-59.5T480-800q-81 0-149.5 43T227-640h-87Zm180 480v-320 320Z"/></svg>
|
||||
|
After Width: | Height: | Size: 434 B |
@@ -81,6 +81,11 @@
|
||||
var(--color-bg-brand-base),
|
||||
var(--overlay-strength-brand)
|
||||
)};
|
||||
--color-bg-brand-softer-solid: color-mix(
|
||||
in srgb,
|
||||
var(--color-bg-primary),
|
||||
var(--color-bg-brand-base) var(--overlay-strength-brand)
|
||||
);
|
||||
|
||||
// Error
|
||||
--overlay-strength-error: 10%;
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
#0012d8,
|
||||
var(--overlay-strength-brand)
|
||||
)};
|
||||
--color-bg-brand-softer-solid: color-mix(
|
||||
in srgb,
|
||||
var(--color-bg-primary),
|
||||
var(--color-bg-brand-base) var(--overlay-strength-brand)
|
||||
);
|
||||
|
||||
// Error
|
||||
--overlay-strength-error: 5%;
|
||||
|
||||
Reference in New Issue
Block a user