mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
[Glitch] Profile editing: Visual fixes
Port 2d4b5b6c51 to glitch-soc
Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
margin: 6px 0;
|
||||
background-color: transparent;
|
||||
appearance: none;
|
||||
display: block;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
|
||||
@@ -14,7 +14,9 @@ export type RangeInputProps = Omit<
|
||||
markers?: { value: number; label: string }[] | number[];
|
||||
};
|
||||
|
||||
interface Props extends RangeInputProps, CommonFieldWrapperProps {}
|
||||
interface Props extends RangeInputProps, CommonFieldWrapperProps {
|
||||
inputPlacement?: 'inline-start' | 'inline-end'; // TODO: Move this to the common field wrapper props for other fields.
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple form field for single-line text.
|
||||
@@ -25,7 +27,16 @@ interface Props extends RangeInputProps, CommonFieldWrapperProps {}
|
||||
|
||||
export const RangeInputField = forwardRef<HTMLInputElement, Props>(
|
||||
(
|
||||
{ id, label, hint, status, required, wrapperClassName, ...otherProps },
|
||||
{
|
||||
id,
|
||||
label,
|
||||
hint,
|
||||
status,
|
||||
required,
|
||||
wrapperClassName,
|
||||
inputPlacement,
|
||||
...otherProps
|
||||
},
|
||||
ref,
|
||||
) => (
|
||||
<FormFieldWrapper
|
||||
@@ -34,6 +45,7 @@ export const RangeInputField = forwardRef<HTMLInputElement, Props>(
|
||||
required={required}
|
||||
status={status}
|
||||
inputId={id}
|
||||
inputPlacement={inputPlacement}
|
||||
className={wrapperClassName}
|
||||
>
|
||||
{(inputProps) => <RangeInput {...otherProps} {...inputProps} ref={ref} />}
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Column } from '@/flavours/glitch/components/column';
|
||||
@@ -36,22 +37,27 @@ export const AccountEditColumn: FC<{
|
||||
const { multiColumn } = useColumnsContext();
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
||||
<ColumnHeader
|
||||
title={title}
|
||||
className={classes.columnHeader}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<Link to={to} className='button'>
|
||||
<FormattedMessage
|
||||
id='account_edit.column_button'
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<>
|
||||
<Column bindToDocument={!multiColumn} className={classes.column}>
|
||||
<ColumnHeader
|
||||
title={title}
|
||||
className={classes.columnHeader}
|
||||
showBackButton
|
||||
extraButton={
|
||||
<Link to={to} className='button'>
|
||||
<FormattedMessage
|
||||
id='account_edit.column_button'
|
||||
defaultMessage='Done'
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
|
||||
{children}
|
||||
</Column>
|
||||
{children}
|
||||
</Column>
|
||||
<Helmet>
|
||||
<title>{title}</title>
|
||||
</Helmet>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import type { FC, MouseEventHandler } from 'react';
|
||||
|
||||
import type { MessageDescriptor } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
@@ -12,43 +9,19 @@ import EditIcon from '@/material-icons/400-24px/edit.svg?react';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
const messages = defineMessages({
|
||||
add: {
|
||||
id: 'account_edit.button.add',
|
||||
defaultMessage: 'Add {item}',
|
||||
},
|
||||
edit: {
|
||||
id: 'account_edit.button.edit',
|
||||
defaultMessage: 'Edit {item}',
|
||||
},
|
||||
delete: {
|
||||
id: 'account_edit.button.delete',
|
||||
defaultMessage: 'Delete {item}',
|
||||
},
|
||||
});
|
||||
|
||||
export interface EditButtonProps {
|
||||
onClick: MouseEventHandler;
|
||||
item: string | MessageDescriptor;
|
||||
edit?: boolean;
|
||||
label: string;
|
||||
icon?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const EditButton: FC<EditButtonProps> = ({
|
||||
onClick,
|
||||
item,
|
||||
edit = false,
|
||||
icon = edit,
|
||||
label,
|
||||
icon = false,
|
||||
disabled,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const itemText = typeof item === 'string' ? item : intl.formatMessage(item);
|
||||
const label = intl.formatMessage(messages[edit ? 'edit' : 'add'], {
|
||||
item: itemText,
|
||||
});
|
||||
|
||||
if (icon) {
|
||||
return (
|
||||
<EditIconButton title={label} onClick={onClick} disabled={disabled} />
|
||||
@@ -83,18 +56,15 @@ export const EditIconButton: FC<{
|
||||
|
||||
export const DeleteIconButton: FC<{
|
||||
onClick: MouseEventHandler;
|
||||
item: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}> = ({ onClick, item, disabled }) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<IconButton
|
||||
icon='delete'
|
||||
iconComponent={DeleteIcon}
|
||||
onClick={onClick}
|
||||
className={classNames(classes.editButton, classes.deleteButton)}
|
||||
title={intl.formatMessage(messages.delete, { item })}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}> = ({ onClick, label, disabled }) => (
|
||||
<IconButton
|
||||
icon='delete'
|
||||
iconComponent={DeleteIcon}
|
||||
onClick={onClick}
|
||||
className={classNames(classes.editButton, classes.deleteButton)}
|
||||
title={label}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import type { FC } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { openModal } from '@/flavours/glitch/actions/modal';
|
||||
import { useAppDispatch } from '@/flavours/glitch/store';
|
||||
|
||||
import { EditButton, DeleteIconButton } from './edit_button';
|
||||
|
||||
export const AccountFieldActions: FC<{ item: string; id: string }> = ({
|
||||
item,
|
||||
id,
|
||||
}) => {
|
||||
const messages = defineMessages({
|
||||
edit: {
|
||||
id: 'account_edit.field_actions.edit',
|
||||
defaultMessage: 'Edit field',
|
||||
},
|
||||
delete: {
|
||||
id: 'account_edit.field_actions.delete',
|
||||
defaultMessage: 'Delete field',
|
||||
},
|
||||
});
|
||||
|
||||
export const AccountFieldActions: FC<{ id: string }> = ({ id }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleEdit = useCallback(() => {
|
||||
dispatch(
|
||||
@@ -28,10 +38,19 @@ export const AccountFieldActions: FC<{ item: string; id: string }> = ({
|
||||
);
|
||||
}, [dispatch, id]);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<EditButton item={item} edit onClick={handleEdit} />
|
||||
<DeleteIconButton item={item} onClick={handleDelete} />
|
||||
<EditButton
|
||||
label={intl.formatMessage(messages.edit)}
|
||||
icon
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
<DeleteIconButton
|
||||
label={intl.formatMessage(messages.delete)}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classes from '../styles.module.scss';
|
||||
|
||||
import { DeleteIconButton, EditButton } from './edit_button';
|
||||
@@ -50,6 +52,17 @@ type AccountEditItemButtonsProps<Item extends AnyItem = AnyItem> = Pick<
|
||||
'onEdit' | 'onDelete' | 'disabled'
|
||||
> & { item: Item };
|
||||
|
||||
const messages = defineMessages({
|
||||
edit: {
|
||||
id: 'account_edit.item_list.edit',
|
||||
defaultMessage: 'Edit {name}',
|
||||
},
|
||||
delete: {
|
||||
id: 'account_edit.item_list.delete',
|
||||
defaultMessage: 'Delete {name}',
|
||||
},
|
||||
});
|
||||
|
||||
const AccountEditItemButtons = <Item extends AnyItem>({
|
||||
item,
|
||||
onDelete,
|
||||
@@ -63,6 +76,8 @@ const AccountEditItemButtons = <Item extends AnyItem>({
|
||||
onDelete?.(item);
|
||||
}, [item, onDelete]);
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
if (!onEdit && !onDelete) {
|
||||
return null;
|
||||
}
|
||||
@@ -71,15 +86,15 @@ const AccountEditItemButtons = <Item extends AnyItem>({
|
||||
<div className={classes.itemListButtons}>
|
||||
{onEdit && (
|
||||
<EditButton
|
||||
edit
|
||||
item={item.name}
|
||||
icon
|
||||
label={intl.formatMessage(messages.edit, { name: item.name })}
|
||||
disabled={disabled}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
)}
|
||||
{onDelete && (
|
||||
<DeleteIconButton
|
||||
item={item.name}
|
||||
label={intl.formatMessage(messages.delete, { name: item.name })}
|
||||
disabled={disabled}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { Callout } from '@/flavours/glitch/components/callout';
|
||||
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
||||
import { Tag } from '@/flavours/glitch/components/tags/tag';
|
||||
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
||||
@@ -28,17 +29,25 @@ import classes from './styles.module.scss';
|
||||
const messages = defineMessages({
|
||||
columnTitle: {
|
||||
id: 'account_edit_tags.column_title',
|
||||
defaultMessage: 'Edit featured hashtags',
|
||||
defaultMessage: 'Edit Tags',
|
||||
},
|
||||
});
|
||||
|
||||
const selectTags = createAppSelector(
|
||||
[(state) => state.profileEdit],
|
||||
(profileEdit) => ({
|
||||
[
|
||||
(state) => state.profileEdit,
|
||||
(state) =>
|
||||
state.server.getIn(
|
||||
['server', 'accounts', 'max_featured_tags'],
|
||||
10,
|
||||
) as number,
|
||||
],
|
||||
(profileEdit, maxTags) => ({
|
||||
tags: profileEdit.profile?.featuredTags ?? [],
|
||||
tagSuggestions: profileEdit.tagSuggestions ?? [],
|
||||
isLoading: !profileEdit.profile || !profileEdit.tagSuggestions,
|
||||
isPending: profileEdit.isPending,
|
||||
maxTags,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -47,7 +56,7 @@ export const AccountEditFeaturedTags: FC = () => {
|
||||
const account = useAccount(accountId);
|
||||
const intl = useIntl();
|
||||
|
||||
const { tags, tagSuggestions, isLoading, isPending } =
|
||||
const { tags, tagSuggestions, isLoading, isPending, maxTags } =
|
||||
useAppSelector(selectTags);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
@@ -67,6 +76,8 @@ export const AccountEditFeaturedTags: FC = () => {
|
||||
return <AccountEditEmptyColumn notFound={!accountId} />;
|
||||
}
|
||||
|
||||
const canAddMoreTags = tags.length < maxTags;
|
||||
|
||||
return (
|
||||
<AccountEditColumn
|
||||
title={intl.formatMessage(messages.columnTitle)}
|
||||
@@ -79,9 +90,9 @@ export const AccountEditFeaturedTags: FC = () => {
|
||||
tagName='p'
|
||||
/>
|
||||
|
||||
<AccountEditTagSearch />
|
||||
{canAddMoreTags && <AccountEditTagSearch />}
|
||||
|
||||
{tagSuggestions.length > 0 && (
|
||||
{tagSuggestions.length > 0 && canAddMoreTags && (
|
||||
<div className={classes.tagSuggestions}>
|
||||
<FormattedMessage
|
||||
id='account_edit_tags.suggestions'
|
||||
@@ -93,6 +104,15 @@ export const AccountEditFeaturedTags: FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!canAddMoreTags && (
|
||||
<Callout icon={false} className={classes.maxTagsWarning}>
|
||||
<FormattedMessage
|
||||
id='account_edit_tags.max_tags_reached'
|
||||
defaultMessage='You have reached the maximum number of featured hashtags.'
|
||||
/>
|
||||
</Callout>
|
||||
)}
|
||||
|
||||
{isLoading && <LoadingIndicator />}
|
||||
|
||||
<AccountEditItemList
|
||||
|
||||
@@ -42,6 +42,14 @@ export const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'Your display name is how your name appears on your profile and in timelines.',
|
||||
},
|
||||
displayNameAddLabel: {
|
||||
id: 'account_edit.display_name.add_label',
|
||||
defaultMessage: 'Add display name',
|
||||
},
|
||||
displayNameEditLabel: {
|
||||
id: 'account_edit.display_name.edit_label',
|
||||
defaultMessage: 'Edit display name',
|
||||
},
|
||||
bioTitle: {
|
||||
id: 'account_edit.bio.title',
|
||||
defaultMessage: 'Bio',
|
||||
@@ -50,6 +58,14 @@ export const messages = defineMessages({
|
||||
id: 'account_edit.bio.placeholder',
|
||||
defaultMessage: 'Add a short introduction to help others identify you.',
|
||||
},
|
||||
bioAddLabel: {
|
||||
id: 'account_edit.bio.label',
|
||||
defaultMessage: 'Add bio',
|
||||
},
|
||||
bioEditLabel: {
|
||||
id: 'account_edit.bio.edit_label',
|
||||
defaultMessage: 'Edit bio',
|
||||
},
|
||||
customFieldsTitle: {
|
||||
id: 'account_edit.custom_fields.title',
|
||||
defaultMessage: 'Custom fields',
|
||||
@@ -59,9 +75,13 @@ export const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'Add your pronouns, external links, or anything else you’d like to share.',
|
||||
},
|
||||
customFieldsName: {
|
||||
id: 'account_edit.custom_fields.name',
|
||||
defaultMessage: 'field',
|
||||
customFieldsAddLabel: {
|
||||
id: 'account_edit.custom_fields.add_label',
|
||||
defaultMessage: 'Add field',
|
||||
},
|
||||
customFieldsEditLabel: {
|
||||
id: 'account_edit.custom_fields.edit_label',
|
||||
defaultMessage: 'Edit field',
|
||||
},
|
||||
customFieldsTipTitle: {
|
||||
id: 'account_edit.custom_fields.tip_title',
|
||||
@@ -76,9 +96,9 @@ export const messages = defineMessages({
|
||||
defaultMessage:
|
||||
'Help others identify, and have quick access to, your favorite topics.',
|
||||
},
|
||||
featuredHashtagsItem: {
|
||||
id: 'account_edit.featured_hashtags.item',
|
||||
defaultMessage: 'hashtags',
|
||||
featuredHashtagsEditLabel: {
|
||||
id: 'account_edit.featured_hashtags.edit_label',
|
||||
defaultMessage: 'Add hashtags',
|
||||
},
|
||||
profileTabTitle: {
|
||||
id: 'account_edit.profile_tab.title',
|
||||
@@ -182,8 +202,12 @@ export const AccountEdit: FC = () => {
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleNameEdit}
|
||||
item={messages.displayNameTitle}
|
||||
edit={hasName}
|
||||
label={intl.formatMessage(
|
||||
hasName
|
||||
? messages.displayNameEditLabel
|
||||
: messages.displayNameAddLabel,
|
||||
)}
|
||||
icon={hasName}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -197,8 +221,10 @@ export const AccountEdit: FC = () => {
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleBioEdit}
|
||||
item={messages.bioTitle}
|
||||
edit={hasBio}
|
||||
label={intl.formatMessage(
|
||||
hasBio ? messages.bioEditLabel : messages.bioAddLabel,
|
||||
)}
|
||||
icon={hasBio}
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -214,7 +240,7 @@ export const AccountEdit: FC = () => {
|
||||
description={messages.customFieldsPlaceholder}
|
||||
showDescription={!hasFields}
|
||||
buttons={
|
||||
<>
|
||||
<div className={classes.fieldButtons}>
|
||||
<Button
|
||||
className={classes.editButton}
|
||||
onClick={handleCustomFieldReorder}
|
||||
@@ -226,11 +252,11 @@ export const AccountEdit: FC = () => {
|
||||
/>
|
||||
</Button>
|
||||
<EditButton
|
||||
item={messages.customFieldsName}
|
||||
label={intl.formatMessage(messages.customFieldsAddLabel)}
|
||||
onClick={handleCustomFieldAdd}
|
||||
disabled={profile.fields.length >= maxFieldCount}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{hasFields && (
|
||||
@@ -240,10 +266,7 @@ export const AccountEdit: FC = () => {
|
||||
<div>
|
||||
<AccountField {...field} {...htmlHandlers} />
|
||||
</div>
|
||||
<AccountFieldActions
|
||||
item={intl.formatMessage(messages.customFieldsName)}
|
||||
id={field.id}
|
||||
/>
|
||||
<AccountFieldActions id={field.id} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
@@ -278,8 +301,8 @@ export const AccountEdit: FC = () => {
|
||||
buttons={
|
||||
<EditButton
|
||||
onClick={handleFeaturedTagsEdit}
|
||||
edit={hasTags}
|
||||
item={messages.featuredHashtagsItem}
|
||||
icon={hasTags}
|
||||
label={intl.formatMessage(messages.featuredHashtagsEditLabel)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
||||
import { closeModal } from '@/flavours/glitch/actions/modal';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import { Callout } from '@/flavours/glitch/components/callout';
|
||||
import { EmojiTextInputField } from '@/flavours/glitch/components/form_fields';
|
||||
@@ -51,14 +58,19 @@ const messages = defineMessages({
|
||||
id: 'account_edit.field_edit_modal.value_hint',
|
||||
defaultMessage: 'E.g. “https://example.me”',
|
||||
},
|
||||
limitHeader: {
|
||||
id: 'account_edit.field_edit_modal.limit_header',
|
||||
defaultMessage: 'Recommended character limit exceeded',
|
||||
},
|
||||
save: {
|
||||
id: 'account_edit.save',
|
||||
defaultMessage: 'Save',
|
||||
},
|
||||
discardMessage: {
|
||||
id: 'account_edit.field_edit_modal.discard_message',
|
||||
defaultMessage:
|
||||
'You have unsaved changes. Are you sure you want to discard them?',
|
||||
},
|
||||
discardConfirm: {
|
||||
id: 'account_edit.field_edit_modal.discard_confirm',
|
||||
defaultMessage: 'Discard',
|
||||
},
|
||||
});
|
||||
|
||||
// We have two different values- the hard limit set by the server,
|
||||
@@ -83,19 +95,39 @@ const selectEmojiCodes = createAppSelector(
|
||||
(emojis) => emojis.map((emoji) => emoji.get('shortcode')).toArray(),
|
||||
);
|
||||
|
||||
export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
|
||||
onClose,
|
||||
fieldKey,
|
||||
}) => {
|
||||
interface ConfirmationMessage {
|
||||
message: string;
|
||||
confirm: string;
|
||||
props: { fieldKey?: string; lastLabel: string; lastValue: string };
|
||||
}
|
||||
|
||||
interface ModalRef {
|
||||
getCloseConfirmationMessage: () => null | ConfirmationMessage;
|
||||
}
|
||||
|
||||
export const EditFieldModal = forwardRef<
|
||||
ModalRef,
|
||||
DialogModalProps & {
|
||||
fieldKey?: string;
|
||||
lastLabel?: string;
|
||||
lastValue?: string;
|
||||
}
|
||||
>(({ onClose, fieldKey, lastLabel, lastValue }, ref) => {
|
||||
const intl = useIntl();
|
||||
const field = useAppSelector((state) => selectFieldById(state, fieldKey));
|
||||
const [newLabel, setNewLabel] = useState(field?.name ?? '');
|
||||
const [newValue, setNewValue] = useState(field?.value ?? '');
|
||||
const oldLabel = lastLabel ?? field?.name;
|
||||
const oldValue = lastValue ?? field?.value;
|
||||
const [newLabel, setNewLabel] = useState(oldLabel ?? '');
|
||||
const [newValue, setNewValue] = useState(oldValue ?? '');
|
||||
const isDirty = newLabel !== oldLabel || newValue !== oldValue;
|
||||
|
||||
const { nameLimit, valueLimit } = useAppSelector(selectFieldLimits);
|
||||
const isPending = useAppSelector((state) => state.profileEdit.isPending);
|
||||
|
||||
const disabled =
|
||||
!newLabel.trim() ||
|
||||
!newValue.trim() ||
|
||||
!isDirty ||
|
||||
!nameLimit ||
|
||||
!valueLimit ||
|
||||
newLabel.length > nameLimit ||
|
||||
@@ -122,11 +154,41 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
|
||||
}
|
||||
void dispatch(
|
||||
updateField({ id: fieldKey, name: newLabel, value: newValue }),
|
||||
).then(onClose);
|
||||
}, [disabled, dispatch, fieldKey, isPending, newLabel, newValue, onClose]);
|
||||
).then(() => {
|
||||
// Close without confirmation.
|
||||
dispatch(
|
||||
closeModal({
|
||||
modalType: 'ACCOUNT_EDIT_FIELD_EDIT',
|
||||
ignoreFocus: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
}, [disabled, dispatch, fieldKey, isPending, newLabel, newValue]);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
getCloseConfirmationMessage: () => {
|
||||
if (!newLabel || !newValue || !isDirty) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
message: intl.formatMessage(messages.discardMessage),
|
||||
confirm: intl.formatMessage(messages.discardConfirm),
|
||||
props: {
|
||||
fieldKey,
|
||||
lastLabel: newLabel,
|
||||
lastValue: newValue,
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
[fieldKey, intl, isDirty, newLabel, newValue],
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
noCloseOnConfirm
|
||||
onClose={onClose}
|
||||
title={
|
||||
field
|
||||
@@ -170,13 +232,10 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
|
||||
|
||||
{(newLabel.length > RECOMMENDED_LIMIT ||
|
||||
newValue.length > RECOMMENDED_LIMIT) && (
|
||||
<Callout
|
||||
variant='warning'
|
||||
title={intl.formatMessage(messages.limitHeader)}
|
||||
>
|
||||
<Callout variant='warning'>
|
||||
<FormattedMessage
|
||||
id='account_edit.field_edit_modal.limit_message'
|
||||
defaultMessage='Mobile users might not see your field in full.'
|
||||
id='account_edit.field_edit_modal.limit_warning'
|
||||
defaultMessage='Recommended character limit exceeded. Mobile users might not see your field in full.'
|
||||
/>
|
||||
</Callout>
|
||||
)}
|
||||
@@ -195,7 +254,8 @@ export const EditFieldModal: FC<DialogModalProps & { fieldKey?: string }> = ({
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
});
|
||||
EditFieldModal.displayName = 'EditFieldModal';
|
||||
|
||||
export const DeleteFieldModal: FC<DialogModalProps & { fieldKey: string }> = ({
|
||||
onClose,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { CharacterCounter } from '@/flavours/glitch/components/character_counter';
|
||||
import { Details } from '@/flavours/glitch/components/details';
|
||||
import { TextAreaField } from '@/flavours/glitch/components/form_fields';
|
||||
import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator';
|
||||
@@ -69,6 +68,7 @@ export const ImageAltModal: FC<
|
||||
imageSrc={imageSrc}
|
||||
altText={altText}
|
||||
onChange={setAltText}
|
||||
hideTip={location === 'header'}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
@@ -79,7 +79,8 @@ export const ImageAltTextField: FC<{
|
||||
imageSrc: string;
|
||||
altText: string;
|
||||
onChange: (altText: string) => void;
|
||||
}> = ({ imageSrc, altText, onChange }) => {
|
||||
hideTip?: boolean;
|
||||
}> = ({ imageSrc, altText, onChange, hideTip }) => {
|
||||
const altLimit = useAppSelector(
|
||||
(state) =>
|
||||
state.server.getIn(
|
||||
@@ -99,49 +100,45 @@ export const ImageAltTextField: FC<{
|
||||
<>
|
||||
<img src={imageSrc} alt='' className={classes.altImage} />
|
||||
|
||||
<div>
|
||||
<TextAreaField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.text_label'
|
||||
defaultMessage='Alt text'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.text_hint'
|
||||
defaultMessage='Alt text helps screen reader users to understand your content.'
|
||||
/>
|
||||
}
|
||||
onChange={handleChange}
|
||||
value={altText}
|
||||
/>
|
||||
<CharacterCounter
|
||||
currentString={altText}
|
||||
maxLength={altLimit}
|
||||
className={classes.altCounter}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Details
|
||||
summary={
|
||||
<TextAreaField
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_title'
|
||||
defaultMessage='Tips: Alt text for profile photos'
|
||||
id='account_edit.image_alt_modal.text_label'
|
||||
defaultMessage='Alt text'
|
||||
/>
|
||||
}
|
||||
className={classes.altHint}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_content'
|
||||
defaultMessage='DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>'
|
||||
values={{
|
||||
ul: (chunks) => <ul>{chunks}</ul>,
|
||||
li: (chunks) => <li>{chunks}</li>,
|
||||
}}
|
||||
tagName='div'
|
||||
/>
|
||||
</Details>
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.text_hint'
|
||||
defaultMessage='Alt text helps screen reader users to understand your content.'
|
||||
/>
|
||||
}
|
||||
onChange={handleChange}
|
||||
value={altText}
|
||||
maxLength={altLimit}
|
||||
/>
|
||||
|
||||
{!hideTip && (
|
||||
<Details
|
||||
summary={
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_title'
|
||||
defaultMessage='Tips: Alt text for profile photos'
|
||||
/>
|
||||
}
|
||||
className={classes.altHint}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='account_edit.image_alt_modal.details_content'
|
||||
defaultMessage='DO: <ul> <li>Describe yourself as pictured</li> <li>Use third person language (e.g. “Alex” instead of “me”)</li> <li>Be succinct – a few words is often enough</li> </ul> DON’T: <ul> <li>Start with “Photo of” – it’s redundant for screen readers</li> </ul> EXAMPLE: <ul> <li>“Alex wearing a green shirt and glasses”</li> </ul>'
|
||||
values={{
|
||||
ul: (chunks) => <ul>{chunks}</ul>,
|
||||
li: (chunks) => <li>{chunks}</li>,
|
||||
}}
|
||||
tagName='div'
|
||||
/>
|
||||
</Details>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { ChangeEventHandler, FC } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import type { Area } from 'react-easy-crop';
|
||||
import Cropper from 'react-easy-crop';
|
||||
|
||||
import { setDragUploadEnabled } from '@/flavours/glitch/actions/compose_typed';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import { RangeInput } from '@/flavours/glitch/components/form_fields/range_input_field';
|
||||
import { RangeInputField } from '@/flavours/glitch/components/form_fields/range_input_field';
|
||||
import {
|
||||
selectImageInfo,
|
||||
uploadImage,
|
||||
@@ -24,16 +24,42 @@ import classes from './styles.module.scss';
|
||||
|
||||
import 'react-easy-crop/react-easy-crop.css';
|
||||
|
||||
const messages = defineMessages({
|
||||
avatarAdd: {
|
||||
id: 'account_edit.upload_modal.title_add.avatar',
|
||||
defaultMessage: 'Add profile photo',
|
||||
},
|
||||
headerAdd: {
|
||||
id: 'account_edit.upload_modal.title_add.header',
|
||||
defaultMessage: 'Add cover photo',
|
||||
},
|
||||
avatarReplace: {
|
||||
id: 'account_edit.upload_modal.title_replace.avatar',
|
||||
defaultMessage: 'Replace profile photo',
|
||||
},
|
||||
headerReplace: {
|
||||
id: 'account_edit.upload_modal.title_replace.header',
|
||||
defaultMessage: 'Replace cover photo',
|
||||
},
|
||||
zoomLabel: {
|
||||
id: 'account_edit.upload_modal.step_crop.zoom',
|
||||
defaultMessage: 'Zoom',
|
||||
},
|
||||
});
|
||||
|
||||
export const ImageUploadModal: FC<
|
||||
DialogModalProps & { location: ImageLocation }
|
||||
> = ({ onClose, location }) => {
|
||||
const { src: oldSrc } = useAppSelector((state) =>
|
||||
selectImageInfo(state, location),
|
||||
);
|
||||
const hasImage = !!oldSrc;
|
||||
const [step, setStep] = useState<'select' | 'crop' | 'alt'>('select');
|
||||
const intl = useIntl();
|
||||
const title = intl.formatMessage(
|
||||
oldSrc ? messages[`${location}Replace`] : messages[`${location}Add`],
|
||||
);
|
||||
|
||||
// State for individual steps.
|
||||
const [step, setStep] = useState<'select' | 'crop' | 'alt'>('select');
|
||||
const [imageSrc, setImageSrc] = useState<string | null>(null);
|
||||
const [imageBlob, setImageBlob] = useState<Blob | null>(null);
|
||||
|
||||
@@ -94,19 +120,7 @@ export const ImageUploadModal: FC<
|
||||
|
||||
return (
|
||||
<DialogModal
|
||||
title={
|
||||
hasImage ? (
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.title_replace'
|
||||
defaultMessage='Replace profile photo'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='account_edit.upload_modal.title_add'
|
||||
defaultMessage='Add profile photo'
|
||||
/>
|
||||
)
|
||||
}
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
wrapperClassName={classes.uploadWrapper}
|
||||
noCancelButton
|
||||
@@ -124,6 +138,7 @@ export const ImageUploadModal: FC<
|
||||
)}
|
||||
{step === 'alt' && imageBlob && (
|
||||
<StepAlt
|
||||
location={location}
|
||||
imageBlob={imageBlob}
|
||||
onCancel={handleCancel}
|
||||
onComplete={handleSave}
|
||||
@@ -275,11 +290,6 @@ const StepUpload: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const zoomLabel = defineMessage({
|
||||
id: 'account_edit.upload_modal.step_crop.zoom',
|
||||
defaultMessage: 'Zoom',
|
||||
});
|
||||
|
||||
const StepCrop: FC<{
|
||||
src: string;
|
||||
location: ImageLocation;
|
||||
@@ -322,14 +332,15 @@ const StepCrop: FC<{
|
||||
</div>
|
||||
|
||||
<div className={classes.cropActions}>
|
||||
<RangeInput
|
||||
<RangeInputField
|
||||
label={intl.formatMessage(messages.zoomLabel)}
|
||||
min={1}
|
||||
max={3}
|
||||
step={0.1}
|
||||
value={zoom}
|
||||
onChange={handleZoomChange}
|
||||
className={classes.zoomControl}
|
||||
aria-label={intl.formatMessage(zoomLabel)}
|
||||
wrapperClassName={classes.zoomControl}
|
||||
inputPlacement='inline-end'
|
||||
/>
|
||||
<Button onClick={onCancel} secondary>
|
||||
<FormattedMessage
|
||||
@@ -352,7 +363,8 @@ const StepAlt: FC<{
|
||||
imageBlob: Blob;
|
||||
onCancel: () => void;
|
||||
onComplete: (altText: string) => void;
|
||||
}> = ({ imageBlob, onCancel, onComplete }) => {
|
||||
location: ImageLocation;
|
||||
}> = ({ imageBlob, onCancel, onComplete, location }) => {
|
||||
const [altText, setAltText] = useState('');
|
||||
|
||||
const handleComplete = useCallback(() => {
|
||||
@@ -367,6 +379,7 @@ const StepAlt: FC<{
|
||||
imageSrc={imageSrc}
|
||||
altText={altText}
|
||||
onChange={setAltText}
|
||||
hideTip={location === 'header'}
|
||||
/>
|
||||
|
||||
<div className={classes.cropActions}>
|
||||
|
||||
@@ -62,24 +62,26 @@ export const ProfileDisplayModal: FC<DialogModalProps> = ({ onClose }) => {
|
||||
}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
checked={profile.showMediaReplies}
|
||||
onChange={handleToggleChange}
|
||||
disabled={!profile.showMedia || isPending}
|
||||
name='show_media_replies'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.profile_tab.show_media_replies.title'
|
||||
defaultMessage='Include replies on ‘Media’ tab'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.profile_tab.show_media_replies.description'
|
||||
defaultMessage='When enabled, Media tab shows both your posts and replies to other people’s posts.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{profile.showMedia && (
|
||||
<ToggleField
|
||||
checked={profile.showMediaReplies}
|
||||
onChange={handleToggleChange}
|
||||
disabled={isPending}
|
||||
name='show_media_replies'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account_edit.profile_tab.show_media_replies.title'
|
||||
defaultMessage='Include replies on ‘Media’ tab'
|
||||
/>
|
||||
}
|
||||
hint={
|
||||
<FormattedMessage
|
||||
id='account_edit.profile_tab.show_media_replies.description'
|
||||
defaultMessage='When enabled, Media tab shows both your posts and replies to other people’s posts.'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ToggleField
|
||||
checked={profile.showFeatured}
|
||||
|
||||
@@ -113,10 +113,14 @@
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.zoomControl {
|
||||
.zoomControl {
|
||||
margin-right: auto;
|
||||
font-size: 13px;
|
||||
|
||||
input {
|
||||
width: min(100%, 200px);
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,10 +132,6 @@
|
||||
border-radius: var(--avatar-border-radius);
|
||||
}
|
||||
|
||||
.altCounter {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.altHint {
|
||||
ul {
|
||||
padding-left: 1em;
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.fieldButtons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
|
||||
@container (width < 500px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.field {
|
||||
padding: 12px 0;
|
||||
display: flex;
|
||||
@@ -87,7 +97,8 @@
|
||||
}
|
||||
|
||||
.autoComplete,
|
||||
.tagSuggestions {
|
||||
.tagSuggestions,
|
||||
.maxTagsWarning {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,28 +91,24 @@ const RedesignNumberFields: FC<{ accountId: string }> = ({ accountId }) => {
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
<NavLink
|
||||
exact
|
||||
to={`/@${account.acct}/followers`}
|
||||
title={intl.formatNumber(account.followers_count)}
|
||||
>
|
||||
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||
<strong>
|
||||
<ShortNumber value={account.followers_count} />
|
||||
</strong>
|
||||
<ShortNumber value={account.followers_count} />
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<FormattedMessage id='account.following' defaultMessage='Following' />
|
||||
<NavLink
|
||||
exact
|
||||
to={`/@${account.acct}/following`}
|
||||
title={intl.formatNumber(account.following_count)}
|
||||
>
|
||||
<FormattedMessage id='account.following' defaultMessage='Following' />
|
||||
<strong>
|
||||
<ShortNumber value={account.following_count} />
|
||||
</strong>
|
||||
<ShortNumber value={account.following_count} />
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
|
||||
@@ -320,23 +320,22 @@ svg.badgeIcon {
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: unset;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--color-text-brand-soft);
|
||||
}
|
||||
}
|
||||
|
||||
a,
|
||||
strong {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modalCloseButton {
|
||||
|
||||
Reference in New Issue
Block a user