diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.tsx b/app/javascript/flavours/glitch/components/dropdown_menu.tsx index b5e1558f32..6f8525257f 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.tsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.tsx @@ -296,6 +296,7 @@ interface DropdownProps { children?: React.ReactElement; icon?: string; iconComponent?: IconProp; + iconClassName?: string; items?: Item[]; loading?: boolean; title?: string; @@ -326,6 +327,7 @@ export const Dropdown = ({ children, icon, iconComponent, + iconClassName, items, loading, title = 'Menu', @@ -499,6 +501,7 @@ export const Dropdown = ({ iconComponent={iconComponent} title={title} active={open} + className={iconClassName} {...buttonProps} /> ); diff --git a/app/javascript/flavours/glitch/features/account_edit/components/image_edit.tsx b/app/javascript/flavours/glitch/features/account_edit/components/image_edit.tsx new file mode 100644 index 0000000000..fbb00909b3 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/components/image_edit.tsx @@ -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 '@/flavours/glitch/actions/modal'; +import { openModal } from '@/flavours/glitch/actions/modal'; +import { Dropdown } from '@/flavours/glitch/components/dropdown_menu'; +import { IconButton } from '@/flavours/glitch/components/icon_button'; +import type { MenuItem } from '@/flavours/glitch/models/dropdown_menu'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/flavours/glitch/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 ( + + ); + } + + return ( + + ); +}; + +const popperOffset = [0, 6] as OffsetValue; diff --git a/app/javascript/flavours/glitch/features/account_edit/index.tsx b/app/javascript/flavours/glitch/features/account_edit/index.tsx index feb1313c7c..b15f299c55 100644 --- a/app/javascript/flavours/glitch/features/account_edit/index.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/index.tsx @@ -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 = () => {
{headerSrc && } + +
+
+ +
-
diff --git a/app/javascript/flavours/glitch/features/account_edit/modals/image_alt.tsx b/app/javascript/flavours/glitch/features/account_edit/modals/image_alt.tsx new file mode 100644 index 0000000000..92360239de --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/modals/image_alt.tsx @@ -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 ; +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/modals/image_delete.tsx b/app/javascript/flavours/glitch/features/account_edit/modals/image_delete.tsx new file mode 100644 index 0000000000..559ff67439 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/modals/image_delete.tsx @@ -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 ; +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/modals/image_upload.tsx b/app/javascript/flavours/glitch/features/account_edit/modals/image_upload.tsx new file mode 100644 index 0000000000..6eb7b73e6c --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_edit/modals/image_upload.tsx @@ -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 ; +}; diff --git a/app/javascript/flavours/glitch/features/account_edit/modals/index.ts b/app/javascript/flavours/glitch/features/account_edit/modals/index.ts index 861e81f597..9b64300e46 100644 --- a/app/javascript/flavours/glitch/features/account_edit/modals/index.ts +++ b/app/javascript/flavours/glitch/features/account_edit/modals/index.ts @@ -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'; diff --git a/app/javascript/flavours/glitch/features/account_edit/styles.module.scss b/app/javascript/flavours/glitch/features/account_edit/styles.module.scss index bafe1bc5b1..ea4aeb6920 100644 --- a/app/javascript/flavours/glitch/features/account_edit/styles.module.scss +++ b/app/javascript/flavours/glitch/features/account_edit/styles.module.scss @@ -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 { diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss b/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss index 51a7962c76..0d9036265a 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss @@ -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; diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 859cc46ea5..5e52711273 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -110,6 +110,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('@/flavours/glitch/features/account_edit/modals')} type */ diff --git a/app/javascript/flavours/glitch/styles/mastodon/theme/_dark.scss b/app/javascript/flavours/glitch/styles/mastodon/theme/_dark.scss index 9af3385d45..a22c7cc8f4 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/theme/_dark.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/theme/_dark.scss @@ -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%; diff --git a/app/javascript/flavours/glitch/styles/mastodon/theme/_light.scss b/app/javascript/flavours/glitch/styles/mastodon/theme/_light.scss index 64390017b3..47d32320fa 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/theme/_light.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/theme/_light.scss @@ -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%;