Merge remote-tracking branch 'origin/develop' into redirect-root
This commit is contained in:
commit
aba3be798d
66 changed files with 476 additions and 256 deletions
13
CHANGELOG.md
13
CHANGELOG.md
|
@ -6,17 +6,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
||||
### Fixed
|
||||
|
||||
## [3.1.0] - 2023-01-13
|
||||
|
||||
### Added
|
||||
- Compatibility: rudimentary support for Takahē.
|
||||
- UI: added backdrop blur behind modals.
|
||||
- Admin: let admins configure media preview for attachment thumbnails.
|
||||
- Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`.
|
||||
- Admin: redirect the homepage to any URL.
|
||||
- Backups: restored Pleroma backups functionality.
|
||||
- Export: restored "Export data" to CSV.
|
||||
|
||||
### Changed
|
||||
- Posts: letterbox images to 19:6 again.
|
||||
- Status Info: moved context (repost, pinned) to improve UX.
|
||||
- Posts: remove file icon from empty link previews.
|
||||
- Settings: moved "Import data" under settings.
|
||||
|
||||
### Fixed
|
||||
- Layout: use accent color for "floating action button" (mobile compose button).
|
||||
|
@ -32,6 +43,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Modals: fix "View context" button in media modal.
|
||||
- Posts: let unauthenticated users to translate posts if allowed by backend.
|
||||
- Chats: fix jumpy scrollbar.
|
||||
- Composer: fix alignment of icon in submit button.
|
||||
- Login: add a border around QR codes.
|
||||
|
||||
### Removed
|
||||
- Admin: single user mode. Now the homepage can be redirected to any URL.
|
||||
|
|
|
@ -47,11 +47,17 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
|
||||
interface IProfilePopper {
|
||||
condition: boolean,
|
||||
wrapper: (children: any) => React.ReactElement<any, any>
|
||||
wrapper: (children: React.ReactNode) => React.ReactNode
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }): any =>
|
||||
condition ? wrapper(children) : children;
|
||||
const ProfilePopper: React.FC<IProfilePopper> = ({ condition, wrapper, children }) => {
|
||||
return (
|
||||
<>
|
||||
{condition ? wrapper(children) : children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export interface IAccount {
|
||||
account: AccountEntity,
|
||||
|
|
|
@ -30,6 +30,7 @@ interface IAutosuggesteTextarea {
|
|||
onFocus: () => void,
|
||||
onBlur?: () => void,
|
||||
condensed?: boolean,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||
|
|
|
@ -16,6 +16,7 @@ interface IDisplayName {
|
|||
account: Account
|
||||
withSuffix?: boolean
|
||||
withDate?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true, withDate = false }) => {
|
||||
|
|
|
@ -26,7 +26,9 @@ const mapStateToProps = (state: RootState) => {
|
|||
};
|
||||
};
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps>;
|
||||
interface Props extends ReturnType<typeof mapStateToProps> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type State = {
|
||||
hasError: boolean,
|
||||
|
@ -213,4 +215,4 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(ErrorBoundary as any);
|
||||
export default connect(mapStateToProps)(ErrorBoundary);
|
||||
|
|
|
@ -15,7 +15,11 @@ const getNotifTotals = (state: RootState): number => {
|
|||
return notifications + reports + approvals;
|
||||
};
|
||||
|
||||
const Helmet: React.FC = ({ children }) => {
|
||||
interface IHelmet {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const Helmet: React.FC<IHelmet> = ({ children }) => {
|
||||
const instance = useInstance();
|
||||
const { unreadChatsCount } = useStatContext();
|
||||
const unreadCount = useAppSelector((state) => getNotifTotals(state) + unreadChatsCount);
|
||||
|
|
|
@ -18,6 +18,7 @@ interface IHoverRefWrapper {
|
|||
accountId: string,
|
||||
inline?: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Makes a profile hover card appear when the wrapped element is hovered. */
|
||||
|
|
|
@ -17,6 +17,7 @@ interface IHoverStatusWrapper {
|
|||
statusId: any,
|
||||
inline: boolean,
|
||||
className?: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Makes a status hover card appear when the wrapped element is hovered. */
|
||||
|
|
|
@ -7,7 +7,11 @@ import { SelectDropdown } from '../features/forms';
|
|||
import Icon from './icon';
|
||||
import { HStack, Select } from './ui';
|
||||
|
||||
const List: React.FC = ({ children }) => (
|
||||
interface IList {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const List: React.FC<IList> = ({ children }) => (
|
||||
<div className='space-y-0.5'>{children}</div>
|
||||
);
|
||||
|
||||
|
@ -17,6 +21,7 @@ interface IListItem {
|
|||
onClick?(): void,
|
||||
onSelect?(): void
|
||||
isSelected?: boolean
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelect, isSelected }) => {
|
||||
|
|
|
@ -42,6 +42,7 @@ interface IModalRoot {
|
|||
onCancel?: () => void,
|
||||
onClose: (type?: ModalType) => void,
|
||||
type: ModalType,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
|
||||
|
@ -128,10 +129,10 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
});
|
||||
};
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === 'Tab') {
|
||||
const focusable = Array.from(ref.current!.querySelectorAll('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])')).filter((x) => window.getComputedStyle(x).display !== 'none');
|
||||
const index = focusable.indexOf(e.target);
|
||||
const index = focusable.indexOf(e.target as Element);
|
||||
|
||||
let element;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ interface IPullToRefresh {
|
|||
onRefresh?: () => Promise<any>;
|
||||
refreshingContent?: JSX.Element | string;
|
||||
pullingContent?: JSX.Element | string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,7 +28,6 @@ const messages = defineMessages({
|
|||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
|
||||
soapboxConfig: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
|
||||
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
|
||||
accountMigration: { id: 'navigation_bar.account_migration', defaultMessage: 'Move account' },
|
||||
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
|
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
|
||||
|
@ -305,15 +304,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
/>
|
||||
)}
|
||||
|
||||
{features.import && (
|
||||
<SidebarLink
|
||||
to='/settings/import'
|
||||
icon={require('@tabler/icons/cloud-upload.svg')}
|
||||
text={intl.formatMessage(messages.importData)}
|
||||
onClick={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<SidebarLink
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState, useRef, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -119,7 +119,7 @@ const StatusContent: React.FC<IStatusContent> = ({ status, onClick, collapsable
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
maybeSetCollapsed();
|
||||
maybeSetOnlyEmoji();
|
||||
updateStatusLinks();
|
||||
|
|
|
@ -79,6 +79,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
defaultMessage='<hover>Replying to</hover> {accounts}'
|
||||
values={{
|
||||
accounts: <FormattedList type='conjunction' value={accounts} />,
|
||||
// @ts-ignore wtf?
|
||||
hover: (children: React.ReactNode) => {
|
||||
if (hoverable) {
|
||||
return (
|
||||
|
|
|
@ -252,8 +252,10 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
if (hidden) {
|
||||
return (
|
||||
<div ref={node}>
|
||||
{actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])}
|
||||
{actualStatus.content}
|
||||
<>
|
||||
{actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])}
|
||||
{actualStatus.content}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
|||
return <Icon src={icon} className='w-4 h-4' />;
|
||||
};
|
||||
|
||||
const handleClick = React.useCallback((event) => {
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
|
||||
if (onClick && !disabled) {
|
||||
onClick(event);
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ interface ICardHeader {
|
|||
backHref?: string,
|
||||
onBackClick?: (event: React.MouseEvent) => void
|
||||
className?: string
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -91,6 +92,8 @@ const CardTitle: React.FC<ICardTitle> = ({ title }): JSX.Element => (
|
|||
interface ICardBody {
|
||||
/** Classnames for the <div> element. */
|
||||
className?: string
|
||||
/** Children to appear inside the card. */
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** A card's body. */
|
||||
|
|
|
@ -46,6 +46,8 @@ export interface IColumn {
|
|||
className?: string,
|
||||
/** Ref forwarded to column. */
|
||||
ref?: React.Ref<HTMLDivElement>
|
||||
/** Children to display in the column. */
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
/** A backdrop for the main section of the UI. */
|
||||
|
|
|
@ -2,8 +2,12 @@ import React from 'react';
|
|||
|
||||
import HStack from '../hstack/hstack';
|
||||
|
||||
interface IFormActions {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Container element to house form actions. */
|
||||
const FormActions: React.FC = ({ children }) => (
|
||||
const FormActions: React.FC<IFormActions> = ({ children }) => (
|
||||
<HStack space={2} justifyContent='end'>
|
||||
{children}
|
||||
</HStack>
|
||||
|
|
|
@ -14,6 +14,8 @@ interface IFormGroup {
|
|||
hintText?: React.ReactNode,
|
||||
/** Input errors. */
|
||||
errors?: string[]
|
||||
/** Elements to display within the FormGroup. */
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Input container with label. Renders the child. */
|
||||
|
|
|
@ -5,11 +5,13 @@ interface IForm {
|
|||
onSubmit?: (event: React.FormEvent) => void,
|
||||
/** Class name override for the <form> element. */
|
||||
className?: string,
|
||||
/** Elements to display within the Form. */
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Form element with custom styles. */
|
||||
const Form: React.FC<IForm> = ({ onSubmit, children, ...filteredProps }) => {
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (onSubmit) {
|
||||
|
|
|
@ -2,10 +2,21 @@ import classNames from 'clsx';
|
|||
import React from 'react';
|
||||
import StickyBox from 'react-sticky-box';
|
||||
|
||||
interface LayoutComponent extends React.FC {
|
||||
Sidebar: React.FC,
|
||||
interface ISidebar {
|
||||
children: React.ReactNode
|
||||
}
|
||||
interface IAside {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
interface ILayout {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
interface LayoutComponent extends React.FC<ILayout> {
|
||||
Sidebar: React.FC<ISidebar>,
|
||||
Main: React.FC<React.HTMLAttributes<HTMLDivElement>>,
|
||||
Aside: React.FC,
|
||||
Aside: React.FC<IAside>,
|
||||
}
|
||||
|
||||
/** Layout container, to hold Sidebar, Main, and Aside. */
|
||||
|
@ -18,7 +29,7 @@ const Layout: LayoutComponent = ({ children }) => (
|
|||
);
|
||||
|
||||
/** Left sidebar container in the UI. */
|
||||
const Sidebar: React.FC = ({ children }) => (
|
||||
const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
||||
<div className='hidden lg:block lg:col-span-3'>
|
||||
<StickyBox offsetTop={80} className='pb-4'>
|
||||
{children}
|
||||
|
@ -38,7 +49,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
|
|||
);
|
||||
|
||||
/** Right sidebar container in the UI. */
|
||||
const Aside: React.FC = ({ children }) => (
|
||||
const Aside: React.FC<IAside> = ({ children }) => (
|
||||
<aside className='hidden xl:block xl:col-span-3'>
|
||||
<StickyBox offsetTop={80} className='space-y-6 pb-12'>
|
||||
{children}
|
||||
|
|
|
@ -52,6 +52,7 @@ interface IModal {
|
|||
/** Title text for the modal. */
|
||||
title?: React.ReactNode,
|
||||
width?: keyof typeof widths,
|
||||
children?: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Displays a modal dialog box. */
|
||||
|
|
|
@ -21,6 +21,7 @@ interface IAnimatedInterface {
|
|||
onChange(index: number): void,
|
||||
/** Default tab index. */
|
||||
defaultIndex: number
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Tabs with a sliding active state. */
|
||||
|
|
|
@ -7,6 +7,8 @@ import './tooltip.css';
|
|||
interface ITooltip {
|
||||
/** Text to display in the tooltip. */
|
||||
text: string,
|
||||
/** Element to display the tooltip around. */
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const centered = (triggerRect: any, tooltipRect: any) => {
|
||||
|
|
|
@ -12,8 +12,12 @@ const WidgetTitle = ({ title }: IWidgetTitle): JSX.Element => (
|
|||
<Text size='xl' weight='bold' tag='h1'>{title}</Text>
|
||||
);
|
||||
|
||||
interface IWidgetBody {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Body of a widget. */
|
||||
const WidgetBody: React.FC = ({ children }): JSX.Element => (
|
||||
const WidgetBody: React.FC<IWidgetBody> = ({ children }): JSX.Element => (
|
||||
<Stack space={3}>{children}</Stack>
|
||||
);
|
||||
|
||||
|
@ -27,6 +31,7 @@ interface IWidget {
|
|||
/** Text for the action. */
|
||||
actionTitle?: string,
|
||||
action?: JSX.Element,
|
||||
children?: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Sidebar widget. */
|
||||
|
|
|
@ -19,7 +19,11 @@ enum ChatWidgetScreens {
|
|||
CHAT_SETTINGS = 'CHAT_SETTINGS'
|
||||
}
|
||||
|
||||
const ChatProvider: React.FC = ({ children }) => {
|
||||
interface IChatProvider {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ChatProvider: React.FC<IChatProvider> = ({ children }) => {
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
const settings = useSettings();
|
||||
|
|
|
@ -9,7 +9,11 @@ const StatContext = createContext<any>({
|
|||
unreadChatsCount: 0,
|
||||
});
|
||||
|
||||
const StatProvider: React.FC = ({ children }) => {
|
||||
interface IStatProvider {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const StatProvider: React.FC<IStatProvider> = ({ children }) => {
|
||||
const [unreadChatsCount, setUnreadChatsCount] = useState<number>(0);
|
||||
|
||||
const value = useMemo(() => ({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
|
@ -66,10 +66,7 @@ const AccountGallery = () => {
|
|||
const hasMore = useAppSelector((state) => state.timelines.get(`account:${accountId}:media`)?.hasMore);
|
||||
|
||||
const [width, setWidth] = useState(323);
|
||||
|
||||
const handleRef = (c: HTMLDivElement) => {
|
||||
if (c) setWidth(c.offsetWidth);
|
||||
};
|
||||
const node = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleScrollToBottom = () => {
|
||||
if (hasMore) {
|
||||
|
@ -99,6 +96,12 @@ const AccountGallery = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (node.current) {
|
||||
setWidth(node.current.offsetWidth);
|
||||
}
|
||||
}, [node.current]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accountId && accountId !== -1) {
|
||||
dispatch(fetchAccount(accountId));
|
||||
|
@ -140,7 +143,7 @@ const AccountGallery = () => {
|
|||
|
||||
return (
|
||||
<Column label={`@${accountUsername}`} transparent withHeader={false}>
|
||||
<div role='feed' className='account-gallery__container' ref={handleRef}>
|
||||
<div role='feed' className='account-gallery__container' ref={node}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.get(index + 1)?.id} maxId={index > 0 ? (attachments.get(index - 1)?.id || null) : null} onLoadMore={handleLoadMore} />
|
||||
) : (
|
||||
|
|
|
@ -32,7 +32,7 @@ const PasswordResetConfirm = () => {
|
|||
|
||||
const isLoading = status === Statuses.LOADING;
|
||||
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
setStatus(Statuses.LOADING);
|
||||
|
@ -41,7 +41,7 @@ const PasswordResetConfirm = () => {
|
|||
.catch(() => setStatus(Statuses.FAIL));
|
||||
}, [password]);
|
||||
|
||||
const onChange = React.useCallback((event) => {
|
||||
const onChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
setPassword(event.target.value);
|
||||
}, []);
|
||||
|
||||
|
|
|
@ -224,124 +224,126 @@ const RegistrationForm: React.FC<IRegistrationForm> = ({ inviteToken }) => {
|
|||
return (
|
||||
<Form onSubmit={onSubmit} data-testid='registrations-open'>
|
||||
<fieldset disabled={isLoading} className='space-y-3'>
|
||||
<FormGroup
|
||||
hintText={intl.formatMessage(messages.username_hint)}
|
||||
errors={usernameUnavailable ? [intl.formatMessage(messages.usernameUnavailable)] : undefined}
|
||||
>
|
||||
<Input
|
||||
type='text'
|
||||
name='username'
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
pattern='^[a-zA-Z\d_-]+'
|
||||
onChange={onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<Input
|
||||
type='email'
|
||||
name='email'
|
||||
placeholder={intl.formatMessage(messages.email)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onInputChange}
|
||||
value={params.get('email', '')}
|
||||
required
|
||||
/>
|
||||
|
||||
<Input
|
||||
type='password'
|
||||
name='password'
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onPasswordChange}
|
||||
value={params.get('password', '')}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
errors={passwordMismatch ? [intl.formatMessage(messages.passwordMismatch)] : undefined}
|
||||
>
|
||||
<Input
|
||||
type='password'
|
||||
name='password_confirmation'
|
||||
placeholder={intl.formatMessage(messages.confirm)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onPasswordConfirmChange}
|
||||
onBlur={onPasswordConfirmBlur}
|
||||
value={passwordConfirmation}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{birthdayRequired && (
|
||||
<BirthdayInput
|
||||
value={params.get('birthday')}
|
||||
onChange={onBirthdayChange}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
{needsApproval && (
|
||||
<>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
||||
hintText={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
|
||||
hintText={intl.formatMessage(messages.username_hint)}
|
||||
errors={usernameUnavailable ? [intl.formatMessage(messages.usernameUnavailable)] : undefined}
|
||||
>
|
||||
<Textarea
|
||||
name='reason'
|
||||
maxLength={500}
|
||||
onChange={onInputChange}
|
||||
value={params.get('reason', '')}
|
||||
<Input
|
||||
type='text'
|
||||
name='username'
|
||||
placeholder={intl.formatMessage(messages.username)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
pattern='^[a-zA-Z\d_-]+'
|
||||
onChange={onUsernameChange}
|
||||
value={params.get('username', '')}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<CaptchaField
|
||||
onFetch={onFetchCaptcha}
|
||||
onFetchFail={onFetchCaptchaFail}
|
||||
onChange={onInputChange}
|
||||
onClick={onCaptchaClick}
|
||||
idempotencyKey={captchaIdempotencyKey}
|
||||
name='captcha_solution'
|
||||
value={params.get('captcha_solution', '')}
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
labelText={intl.formatMessage(messages.agreement, { tos: <Link to='/about/tos' target='_blank' key={0}>{intl.formatMessage(messages.tos)}</Link> })}
|
||||
>
|
||||
<Checkbox
|
||||
name='agreement'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('agreement', false)}
|
||||
<Input
|
||||
type='email'
|
||||
name='email'
|
||||
placeholder={intl.formatMessage(messages.email)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onInputChange}
|
||||
value={params.get('email', '')}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{supportsEmailList && (
|
||||
<FormGroup labelText={intl.formatMessage(messages.newsletter)}>
|
||||
<Checkbox
|
||||
name='accepts_email_list'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('accepts_email_list', false)}
|
||||
<Input
|
||||
type='password'
|
||||
name='password'
|
||||
placeholder={intl.formatMessage(messages.password)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onPasswordChange}
|
||||
value={params.get('password', '')}
|
||||
required
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
errors={passwordMismatch ? [intl.formatMessage(messages.passwordMismatch)] : undefined}
|
||||
>
|
||||
<Input
|
||||
type='password'
|
||||
name='password_confirmation'
|
||||
placeholder={intl.formatMessage(messages.confirm)}
|
||||
autoComplete='off'
|
||||
autoCorrect='off'
|
||||
autoCapitalize='off'
|
||||
onChange={onPasswordConfirmChange}
|
||||
onBlur={onPasswordConfirmBlur}
|
||||
value={passwordConfirmation}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit'>
|
||||
<FormattedMessage id='registration.sign_up' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
{birthdayRequired && (
|
||||
<BirthdayInput
|
||||
value={params.get('birthday')}
|
||||
onChange={onBirthdayChange}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
{needsApproval && (
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='registration.reason' defaultMessage='Why do you want to join?' />}
|
||||
hintText={<FormattedMessage id='registration.reason_hint' defaultMessage='This will help us review your application' />}
|
||||
>
|
||||
<Textarea
|
||||
name='reason'
|
||||
maxLength={500}
|
||||
onChange={onInputChange}
|
||||
value={params.get('reason', '')}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<CaptchaField
|
||||
onFetch={onFetchCaptcha}
|
||||
onFetchFail={onFetchCaptchaFail}
|
||||
onChange={onInputChange}
|
||||
onClick={onCaptchaClick}
|
||||
idempotencyKey={captchaIdempotencyKey}
|
||||
name='captcha_solution'
|
||||
value={params.get('captcha_solution', '')}
|
||||
/>
|
||||
|
||||
<FormGroup
|
||||
labelText={intl.formatMessage(messages.agreement, { tos: <Link to='/about/tos' target='_blank' key={0}>{intl.formatMessage(messages.tos)}</Link> })}
|
||||
>
|
||||
<Checkbox
|
||||
name='agreement'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('agreement', false)}
|
||||
required
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{supportsEmailList && (
|
||||
<FormGroup labelText={intl.formatMessage(messages.newsletter)}>
|
||||
<Checkbox
|
||||
name='accepts_email_list'
|
||||
onChange={onCheckboxChange}
|
||||
checked={params.get('accepts_email_list', false)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormActions>
|
||||
<Button type='submit'>
|
||||
<FormattedMessage id='registration.sign_up' defaultMessage='Sign up' />
|
||||
</Button>
|
||||
</FormActions>
|
||||
</>
|
||||
</fieldset>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { fetchBackups, createBackup } from 'soapbox/actions/backups';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { Button, Column, FormActions, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
@ -23,22 +22,14 @@ const Backups = () => {
|
|||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const handleCreateBackup: React.MouseEventHandler<HTMLAnchorElement> = e => {
|
||||
const handleCreateBackup: React.MouseEventHandler = e => {
|
||||
dispatch(createBackup());
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const makeColumnMenu = () => {
|
||||
return [{
|
||||
text: intl.formatMessage(messages.create),
|
||||
action: handleCreateBackup,
|
||||
icon: require('@tabler/icons/plus.svg'),
|
||||
}];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchBackups()).then(() => {
|
||||
setIsLoading(true);
|
||||
setIsLoading(false);
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
|
@ -46,16 +37,14 @@ const Backups = () => {
|
|||
|
||||
const emptyMessageAction = (
|
||||
<a href='#' onClick={handleCreateBackup}>
|
||||
{intl.formatMessage(messages.emptyMessageAction)}
|
||||
<Text tag='span' theme='primary' size='sm' className='hover:underline'>
|
||||
{intl.formatMessage(messages.emptyMessageAction)}
|
||||
</Text>
|
||||
</a>
|
||||
);
|
||||
|
||||
return (
|
||||
<Column
|
||||
label={intl.formatMessage(messages.heading)}
|
||||
// @ts-ignore FIXME: make this menu available.
|
||||
menu={makeColumnMenu()}
|
||||
>
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<ScrollableList
|
||||
isLoading={isLoading}
|
||||
showLoading={showLoading}
|
||||
|
@ -64,16 +53,22 @@ const Backups = () => {
|
|||
>
|
||||
{backups.map((backup) => (
|
||||
<div
|
||||
className={classNames('backup', { 'backup--pending': !backup.processed })}
|
||||
className='p-4'
|
||||
key={backup.id}
|
||||
>
|
||||
{backup.processed
|
||||
? <a href={backup.url} target='_blank'>{backup.inserted_at}</a>
|
||||
: <div>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</div>
|
||||
: <Text theme='subtle'>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</Text>
|
||||
}
|
||||
</div>
|
||||
))}
|
||||
</ScrollableList>
|
||||
|
||||
<FormActions>
|
||||
<Button theme='primary' disabled={isLoading} onClick={handleCreateBackup}>
|
||||
{intl.formatMessage(messages.create)}
|
||||
</Button>
|
||||
</FormActions>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,6 +6,8 @@ import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
|||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import useAccountSearch from 'soapbox/queries/search';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
interface IResults {
|
||||
accountSearchResult: ReturnType<typeof useAccountSearch>
|
||||
onSelect(id: string): void
|
||||
|
@ -23,7 +25,7 @@ const Results = ({ accountSearchResult, onSelect }: IResults) => {
|
|||
}
|
||||
};
|
||||
|
||||
const renderAccount = useCallback((_index, account) => (
|
||||
const renderAccount = useCallback((_index: number, account: Account) => (
|
||||
<button
|
||||
key={account.id}
|
||||
type='button'
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from 'soapbox/actions/compose';
|
||||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, HStack, Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures, useInstance, usePrevious } from 'soapbox/hooks';
|
||||
import { isMobile } from 'soapbox/is-mobile';
|
||||
|
@ -241,25 +240,18 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
const disabledButton = disabled || isUploading || isChangingUpload || length(countedText) > maxTootChars || (countedText.length !== 0 && countedText.trim().length === 0 && !anyMedia);
|
||||
const shouldAutoFocus = autoFocus && !showSearch && !isMobile(window.innerWidth);
|
||||
|
||||
let publishText: string | JSX.Element = '';
|
||||
let publishText: string = '';
|
||||
let publishIcon: string | undefined;
|
||||
let textareaPlaceholder: MessageDescriptor;
|
||||
|
||||
if (isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (privacy === 'direct') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/mail.svg')} />
|
||||
{intl.formatMessage(messages.message)}
|
||||
</>
|
||||
);
|
||||
publishText = intl.formatMessage(messages.message);
|
||||
publishIcon = require('@tabler/icons/mail.svg');
|
||||
} else if (privacy === 'private') {
|
||||
publishText = (
|
||||
<>
|
||||
<Icon src={require('@tabler/icons/lock.svg')} />
|
||||
{intl.formatMessage(messages.publish)}
|
||||
</>
|
||||
);
|
||||
publishText = intl.formatMessage(messages.publish);
|
||||
publishIcon = require('@tabler/icons/lock.svg');
|
||||
} else {
|
||||
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
@ -355,7 +347,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
</HStack>
|
||||
)}
|
||||
|
||||
<Button type='submit' theme='primary' text={publishText} disabled={disabledButton} />
|
||||
<Button type='submit' theme='primary' text={publishText} icon={publishIcon} disabled={disabledButton} />
|
||||
</HStack>
|
||||
{/* <HStack alignItems='center' space={4}>
|
||||
</HStack> */}
|
||||
|
|
|
@ -72,8 +72,8 @@ const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
|
|||
|
||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(customEmojis) as Set<string>).sort());
|
||||
|
||||
const handleDocumentClick = useCallback(e => {
|
||||
if (node.current && !node.current.contains(e.target)) {
|
||||
const handleDocumentClick = useCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (node.current && !node.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}, []);
|
||||
|
|
|
@ -19,8 +19,8 @@ const ModifierPickerMenu: React.FC<IModifierPickerMenu> = ({ active, onSelect, o
|
|||
onSelect(+e.currentTarget.getAttribute('data-index')! * 1);
|
||||
};
|
||||
|
||||
const handleDocumentClick = useCallback((e => {
|
||||
if (node.current && !node.current.contains(e.target)) {
|
||||
const handleDocumentClick = useCallback(((e: MouseEvent | TouchEvent) => {
|
||||
if (node.current && !node.current.contains(e.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
}), []);
|
||||
|
|
|
@ -36,7 +36,7 @@ const DetailedCryptoAddress: React.FC<IDetailedCryptoAddress> = ({ address, tick
|
|||
</div>
|
||||
{note && <div className='crypto-address__note'>{note}</div>}
|
||||
<div className='crypto-address__qrcode'>
|
||||
<QRCode value={address} />
|
||||
<QRCode className='rounded-lg' value={address} includeMargin />
|
||||
</div>
|
||||
|
||||
<CopyableInput value={address} />
|
||||
|
|
|
@ -28,7 +28,7 @@ const EditEmail = () => {
|
|||
|
||||
const { email, password } = state;
|
||||
|
||||
const handleInputChange = React.useCallback((event) => {
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
event.persist();
|
||||
|
||||
setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
|
||||
|
|
|
@ -34,7 +34,7 @@ const EditPassword = () => {
|
|||
|
||||
const resetState = () => setState(initialState);
|
||||
|
||||
const handleInputChange = React.useCallback((event) => {
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
event.persist();
|
||||
|
||||
setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
|
||||
|
|
|
@ -11,6 +11,7 @@ interface IInputContainer {
|
|||
type?: string,
|
||||
extraClass?: string,
|
||||
error?: boolean,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export const InputContainer: React.FC<IInputContainer> = (props) => {
|
||||
|
@ -32,6 +33,7 @@ export const InputContainer: React.FC<IInputContainer> = (props) => {
|
|||
interface ILabelInputContainer {
|
||||
label?: React.ReactNode,
|
||||
hint?: React.ReactNode,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export const LabelInputContainer: React.FC<ILabelInputContainer> = ({ label, hint, children }) => {
|
||||
|
@ -128,6 +130,7 @@ interface ISimpleForm {
|
|||
onSubmit?: React.FormEventHandler,
|
||||
acceptCharset?: string,
|
||||
style?: React.CSSProperties,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export const SimpleForm: React.FC<ISimpleForm> = (props) => {
|
||||
|
@ -157,7 +160,11 @@ export const SimpleForm: React.FC<ISimpleForm> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const FieldsGroup: React.FC = ({ children }) => (
|
||||
interface IFieldsGroup {
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
export const FieldsGroup: React.FC<IFieldsGroup> = ({ children }) => (
|
||||
<div className='fields-group'>{children}</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ const OtpConfirmForm: React.FC = () => {
|
|||
});
|
||||
}, []);
|
||||
|
||||
const handleInputChange = useCallback((event) => {
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = useCallback((event) => {
|
||||
event.persist();
|
||||
|
||||
setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
|
||||
|
@ -75,7 +75,7 @@ const OtpConfirmForm: React.FC = () => {
|
|||
</Text>
|
||||
</Stack>
|
||||
|
||||
<QRCode value={state.qrCodeURI} />
|
||||
<QRCode className='rounded-lg' value={state.qrCodeURI} includeMargin />
|
||||
{state.confirmKey}
|
||||
|
||||
<Text weight='semibold' size='lg'>
|
||||
|
|
|
@ -27,6 +27,9 @@ const messages = defineMessages({
|
|||
other: { id: 'settings.other', defaultMessage: 'Other options' },
|
||||
mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' },
|
||||
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
|
||||
backups: { id: 'column.backups', defaultMessage: 'Backups' },
|
||||
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
|
||||
exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
|
||||
});
|
||||
|
||||
/** User settings page. */
|
||||
|
@ -47,6 +50,9 @@ const Settings = () => {
|
|||
const navigateToDeleteAccount = () => history.push('/settings/account');
|
||||
const navigateToMoveAccount = () => history.push('/settings/migration');
|
||||
const navigateToAliases = () => history.push('/settings/aliases');
|
||||
const navigateToBackups = () => history.push('/settings/backups');
|
||||
const navigateToImportData = () => history.push('/settings/import');
|
||||
const navigateToExportData = () => history.push('/settings/export');
|
||||
|
||||
const isMfaEnabled = mfa.getIn(['settings', 'totp']);
|
||||
|
||||
|
@ -130,6 +136,18 @@ const Settings = () => {
|
|||
|
||||
<CardBody>
|
||||
<List>
|
||||
{features.importData && (
|
||||
<ListItem label={intl.formatMessage(messages.importData)} onClick={navigateToImportData} />
|
||||
)}
|
||||
|
||||
{features.exportData && (
|
||||
<ListItem label={intl.formatMessage(messages.exportData)} onClick={navigateToExportData} />
|
||||
)}
|
||||
|
||||
{features.backups && (
|
||||
<ListItem label={intl.formatMessage(messages.backups)} onClick={navigateToBackups} />
|
||||
)}
|
||||
|
||||
{features.federating && (features.accountMoving ? (
|
||||
<ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} />
|
||||
) : features.accountAliases && (
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Layout } from '../../../components/ui';
|
|||
|
||||
interface IColumnsArea {
|
||||
layout: any,
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const ColumnsArea: React.FC<IColumnsArea> = (props) => {
|
||||
|
|
|
@ -13,6 +13,7 @@ interface IFooterLink {
|
|||
to: string,
|
||||
className?: string,
|
||||
onClick?: React.EventHandler<React.MouseEvent>,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const FooterLink: React.FC<IFooterLink> = ({ children, className, ...rest }): JSX.Element => {
|
||||
|
@ -56,9 +57,6 @@ const LinkFooter: React.FC = (): JSX.Element => {
|
|||
{account.locked && (
|
||||
<FooterLink to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></FooterLink>
|
||||
)}
|
||||
{features.import && (
|
||||
<FooterLink to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/logout' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink>
|
||||
</>}
|
||||
</div>
|
||||
|
|
|
@ -21,6 +21,7 @@ const messages = defineMessages({
|
|||
|
||||
interface IProfileDropdown {
|
||||
account: AccountEntity
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
type IMenuItem = {
|
||||
|
|
|
@ -76,9 +76,9 @@ import {
|
|||
EmailConfirmation,
|
||||
DeleteAccount,
|
||||
SoapboxConfig,
|
||||
// ExportData,
|
||||
ExportData,
|
||||
ImportData,
|
||||
// Backups,
|
||||
Backups,
|
||||
MfaForm,
|
||||
ChatIndex,
|
||||
ChatWidget,
|
||||
|
@ -149,7 +149,11 @@ const keyMap = {
|
|||
openMedia: 'a',
|
||||
};
|
||||
|
||||
const SwitchingColumnsArea: React.FC = ({ children }) => {
|
||||
interface ISwitchingColumnsArea {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) => {
|
||||
const features = useFeatures();
|
||||
const { search } = useLocation();
|
||||
|
||||
|
@ -273,11 +277,11 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
|
||||
|
||||
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
|
||||
{/* FIXME: this could DDoS our API? :\ */}
|
||||
{/* <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} /> */}
|
||||
{features.exportData && <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />}
|
||||
{features.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />}
|
||||
{features.accountAliases && <WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />}
|
||||
{features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />}
|
||||
{features.backups && <WrappedRoute path='/settings/backups' page={DefaultPage} component={Backups} content={children} />}
|
||||
<WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} />
|
||||
<WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} />
|
||||
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
|
||||
|
@ -285,7 +289,6 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/settings/mfa' page={DefaultPage} component={MfaForm} exact />
|
||||
<WrappedRoute path='/settings/tokens' page={DefaultPage} component={AuthTokenList} content={children} />
|
||||
<WrappedRoute path='/settings' page={DefaultPage} component={Settings} content={children} />
|
||||
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
|
||||
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
|
||||
|
||||
<WrappedRoute path='/soapbox/admin' staffOnly page={AdminPage} component={Dashboard} content={children} exact />
|
||||
|
@ -314,7 +317,11 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const UI: React.FC = ({ children }) => {
|
||||
interface IUI {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const UI: React.FC<IUI> = ({ children }) => {
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
|
@ -43,7 +43,7 @@ const Registration = () => {
|
|||
const [hasValidPassword, setHasValidPassword] = React.useState<boolean>(false);
|
||||
const { username, password } = state;
|
||||
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
dispatch(createAccount(username, password))
|
||||
|
@ -69,7 +69,7 @@ const Registration = () => {
|
|||
});
|
||||
}, [username, password]);
|
||||
|
||||
const handleInputChange = React.useCallback((event) => {
|
||||
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
event.persist();
|
||||
|
||||
setState((prevState) => ({ ...prevState, [event.target.name]: event.target.value }));
|
||||
|
|
|
@ -29,15 +29,15 @@ const AgeVerification = () => {
|
|||
const isLoading = useAppSelector((state) => state.verification.isLoading) as boolean;
|
||||
const ageMinimum = useAppSelector((state) => state.verification.ageMinimum) as any;
|
||||
|
||||
const [date, setDate] = React.useState('');
|
||||
const [date, setDate] = React.useState<Date>();
|
||||
const isValid = typeof date === 'object';
|
||||
|
||||
const onChange = React.useCallback((date) => setDate(date), []);
|
||||
const onChange = React.useCallback((date: Date) => setDate(date), []);
|
||||
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const birthday = new Date(date);
|
||||
const birthday = new Date(date!);
|
||||
|
||||
if (meetsAgeMinimum(birthday, ageMinimum)) {
|
||||
dispatch(verifyAge(birthday));
|
||||
|
|
|
@ -69,7 +69,9 @@ const EmailVerification = () => {
|
|||
|
||||
const isValid = email.length > 0 && EMAIL_REGEX.test(email);
|
||||
|
||||
const onChange = React.useCallback((event) => setEmail(event.target.value), []);
|
||||
const onChange: React.ChangeEventHandler<HTMLInputElement> = React.useCallback((event) => {
|
||||
setEmail(event.target.value);
|
||||
}, []);
|
||||
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
|
|
@ -39,7 +39,7 @@ const SmsVerification = () => {
|
|||
setPhone(phone);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = React.useCallback((event) => {
|
||||
const handleSubmit: React.FormEventHandler = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!isValid) {
|
||||
|
@ -59,7 +59,7 @@ const SmsVerification = () => {
|
|||
});
|
||||
}, [phone, isValid]);
|
||||
|
||||
const resendVerificationCode = React.useCallback((event) => {
|
||||
const resendVerificationCode: React.MouseEventHandler = React.useCallback((event) => {
|
||||
setAlreadyRequestedAnother(true);
|
||||
handleSubmit(event);
|
||||
}, [isValid]);
|
||||
|
|
|
@ -80,7 +80,7 @@ const customRender = (
|
|||
});
|
||||
|
||||
/** Like renderHook, but with access to the Redux store. */
|
||||
const customRenderHook = <T extends {}>(
|
||||
const customRenderHook = <T extends { children?: React.ReactNode }>(
|
||||
callback: (props?: any) => any,
|
||||
options?: Omit<RenderHookOptions<T>, 'wrapper'>,
|
||||
store?: any,
|
||||
|
|
|
@ -110,12 +110,15 @@
|
|||
"admin.reports.empty_message": "Nessuna segnalazione in corso. Se un profilo verrà segnalato, comparirà qui.",
|
||||
"admin.reports.report_closed_message": "La segnalazione per @{name} è stata chiusa",
|
||||
"admin.reports.report_title": "Segnalazione su {acct}",
|
||||
"admin.software.backend": "Lato server",
|
||||
"admin.software.frontend": "Lato client",
|
||||
"admin.statuses.actions.delete_status": "Elimina pubblicazione",
|
||||
"admin.statuses.actions.mark_status_not_sensitive": "Segna non sensibile",
|
||||
"admin.statuses.actions.mark_status_sensitive": "Segna come sensibile",
|
||||
"admin.statuses.status_deleted_message": "La pubblicazione di @{acct} è stata eliminata",
|
||||
"admin.statuses.status_marked_message_not_sensitive": "La pubblicazione di @{acct} è stata segnata non sensibile",
|
||||
"admin.statuses.status_marked_message_sensitive": "La pubblicazione di @{acct} è stata segnata come sensibile",
|
||||
"admin.theme.title": "Tema",
|
||||
"admin.user_index.empty": "Non hai trovato alcun profilo.",
|
||||
"admin.user_index.search_input_placeholder": "Chi stai cercando?",
|
||||
"admin.users.actions.deactivate_user": "Disattiva @{name}",
|
||||
|
@ -185,10 +188,79 @@
|
|||
"bundle_modal_error.message": "C'è stato un errore mentre questo componente veniva caricato.",
|
||||
"bundle_modal_error.retry": "Riprova",
|
||||
"card.back.label": "Indietro",
|
||||
"chat.actions.send": "Invia",
|
||||
"chat.failed_to_send": "Ahimè! Messaggio non spedito.",
|
||||
"chat.input.placeholder": "Scrivi un messaggio",
|
||||
"chat.new_message.title": "Nuovo messaggio",
|
||||
"chat.page_settings.accepting_messages.label": "Permetti alle persone di iniziare una nuova chat con te",
|
||||
"chat.page_settings.play_sounds.label": "Segnale acustico quando arriva un messaggio",
|
||||
"chat.page_settings.preferences": "Preferenze",
|
||||
"chat.page_settings.privacy": "Privacy",
|
||||
"chat.page_settings.submit": "Salva",
|
||||
"chat.page_settings.title": "Impostazioni dei messaggi",
|
||||
"chat.retry": "Riprovare?",
|
||||
"chat.welcome.accepting_messages.label": "Permetti ad altre persone di contattarti via chat",
|
||||
"chat.welcome.notice": "Puoi modificare queste impostazioni anche dopo.",
|
||||
"chat.welcome.submit": "Salva e continua",
|
||||
"chat.welcome.subtitle": "Scambia messaggi diretti con altre persone.",
|
||||
"chat.welcome.title": "Eccoci nelle chat di {br}!",
|
||||
"chat_composer.unblock": "Sblocca",
|
||||
"chat_list_item.blocked_you": "Questa persona ti ha bloccato",
|
||||
"chat_list_item.blocking": "Hai bloccato questa persona",
|
||||
"chat_message_list.blocked": "Hai bloccato questa persona",
|
||||
"chat_message_list.blockedBy": "Blocco da",
|
||||
"chat_message_list.network_failure.action": "Riprova",
|
||||
"chat_message_list.network_failure.subtitle": "Si è verificato un errore di connessione.",
|
||||
"chat_message_list.network_failure.title": "Ooops!",
|
||||
"chat_message_list_intro.actions.accept": "Acconsenti",
|
||||
"chat_message_list_intro.actions.leave_chat": "Abbandona la chat",
|
||||
"chat_message_list_intro.actions.message_lifespan": "Saranno eliminati i messaggi più vecchi di {day} giorni.",
|
||||
"chat_message_list_intro.actions.report": "Segnala",
|
||||
"chat_message_list_intro.intro": "vuole iniziare una chat con te",
|
||||
"chat_message_list_intro.leave_chat.confirm": "Abbandona la chat",
|
||||
"chat_message_list_intro.leave_chat.heading": "Abbandona la chat",
|
||||
"chat_message_list_intro.leave_chat.message": "Vuoi davvero abbandonare questa chat? Questo comporta che i messaggi contenuti verranno eliminati automaticamente.",
|
||||
"chat_search.blankslate.body": "Cerca qualche persona con cui chattare.",
|
||||
"chat_search.blankslate.title": "Inizia una chat",
|
||||
"chat_search.empty_results_blankslate.action": "Scrivi a qualche persona",
|
||||
"chat_search.empty_results_blankslate.body": "Prova a cercare un'altra persona.",
|
||||
"chat_search.empty_results_blankslate.title": "Nessun risultato",
|
||||
"chat_search.placeholder": "Digita il nome",
|
||||
"chat_search.title": "Messaggi",
|
||||
"chat_settings.auto_delete.14days": "14 giorni",
|
||||
"chat_settings.auto_delete.2minutes": "2 minuti",
|
||||
"chat_settings.auto_delete.30days": "30 giorni",
|
||||
"chat_settings.auto_delete.7days": "7 giorni",
|
||||
"chat_settings.auto_delete.90days": "90 giorni",
|
||||
"chat_settings.auto_delete.days": "{day} giorni",
|
||||
"chat_settings.auto_delete.hint": "I messaggi inviati saranno eliminati automaticamente al termine del periodo selezionato",
|
||||
"chat_settings.auto_delete.label": "Eliminazione automatica dei messaggi",
|
||||
"chat_settings.block.confirm": "Blocca",
|
||||
"chat_settings.block.heading": "Blocca @{acct}",
|
||||
"chat_settings.block.message": "Bloccando questo profilo potrai evitare che la persona sia in grado di scriverti e vedere le tue pubblicazioni. Potrai sbloccarlo successivamente.",
|
||||
"chat_settings.leave.confirm": "Abbandona la chat",
|
||||
"chat_settings.leave.heading": "Abbandona la chat",
|
||||
"chat_settings.leave.message": "Vuoi davvero abbandonare questa chat? Questo comporta che i messaggi contenuti verranno eliminati automaticamente.",
|
||||
"chat_settings.options.block_user": "Blocca @{acct}",
|
||||
"chat_settings.options.leave_chat": "Abbandona la chat",
|
||||
"chat_settings.options.report_user": "Segnala @{acct}",
|
||||
"chat_settings.options.unblock_user": "Sblocca @{acct}",
|
||||
"chat_settings.title": "Dettagli della chat",
|
||||
"chat_settings.unblock.confirm": "Sblocca",
|
||||
"chat_settings.unblock.heading": "Sblocca @{acct}",
|
||||
"chat_settings.unblock.message": "Sbloccando permetterai a questo profile di inviarti messaggi e vedere le tue pubblicazioni.",
|
||||
"chat_window.auto_delete_label": "Eliminazione automatica dopo {day} giorni",
|
||||
"chat_window.auto_delete_tooltip": "Eliminazione automatica dei messaggi dopo {day} dalla relativa spedizione.",
|
||||
"chats.actions.copy": "Copia",
|
||||
"chats.actions.delete": "Elimina",
|
||||
"chats.actions.deleteForMe": "Elimina per me",
|
||||
"chats.actions.more": "Di più",
|
||||
"chats.actions.report": "Segnala il profilo",
|
||||
"chats.dividers.today": "Oggi",
|
||||
"chats.main.blankslate.new_chat": "Scrivi a qualche persona",
|
||||
"chats.main.blankslate.subtitle": "Cerca qualche persona con cui chattare",
|
||||
"chats.main.blankslate.title": "Ancora nessun messaggio",
|
||||
"chats.main.blankslate_with_chats.title": "Seleziona chat",
|
||||
"chats.search_placeholder": "Inizia a chattare con…",
|
||||
"column.admin.awaiting_approval": "Attesa approvazione",
|
||||
"column.admin.dashboard": "Cruscotto",
|
||||
|
@ -216,6 +288,9 @@
|
|||
"column.directory": "Esplora i profili pubblici",
|
||||
"column.domain_blocks": "Domini nascosti",
|
||||
"column.edit_profile": "Modifica del profilo",
|
||||
"column.event_map": "Località dell'evento",
|
||||
"column.event_participants": "Partecipanti all'evento",
|
||||
"column.events": "Eventi",
|
||||
"column.export_data": "Esportazione dati",
|
||||
"column.familiar_followers": "Persone che conosci, che seguono {name}",
|
||||
"column.favourited_statuses": "Pubblicazioni preferite",
|
||||
|
@ -258,6 +333,7 @@
|
|||
"column.pins": "Pubblicazioni selezionate",
|
||||
"column.preferences": "Preferenze di funzionamento",
|
||||
"column.public": "Timeline federata",
|
||||
"column.quotes": "Citazioni",
|
||||
"column.reactions": "Reazioni",
|
||||
"column.reblogs": "Ripubblicazioni",
|
||||
"column.scheduled_statuses": "Pubblicazioni pianificate",
|
||||
|
@ -274,7 +350,30 @@
|
|||
"compose.edit_success": "Hai modificato la pubblicazione",
|
||||
"compose.invalid_schedule": "Devi pianificare le pubblicazioni almeno fra 5 minuti.",
|
||||
"compose.submit_success": "Pubblicazione avvenuta!",
|
||||
"compose_event.create": "Crea",
|
||||
"compose_event.edit_success": "Evento modificato",
|
||||
"compose_event.fields.approval_required": "Voglio confermare manualmente richieste di partecipazione",
|
||||
"compose_event.fields.description_hint": "Puoi usare la formattazione Markdown",
|
||||
"compose_event.fields.description_label": "Descrizione dell'evento",
|
||||
"compose_event.fields.description_placeholder": "Descrizione",
|
||||
"compose_event.fields.end_time_label": "Data di fine evento",
|
||||
"compose_event.fields.end_time_placeholder": "L'evento finisce il…",
|
||||
"compose_event.fields.has_end_time": "L'evento ha una data di conclusione",
|
||||
"compose_event.fields.location_label": "Località dell'evento",
|
||||
"compose_event.fields.name_label": "Nome dell'evento",
|
||||
"compose_event.fields.name_placeholder": "Nome",
|
||||
"compose_event.fields.start_time_label": "Data di inizio dell'evento",
|
||||
"compose_event.fields.start_time_placeholder": "L'evento inizia il…",
|
||||
"compose_event.participation_requests.authorize": "Autorizza",
|
||||
"compose_event.participation_requests.authorize_success": "Persona accettata",
|
||||
"compose_event.participation_requests.reject": "Rifiuta",
|
||||
"compose_event.participation_requests.reject_success": "Persona rifiutata",
|
||||
"compose_event.submit_success": "Hai creato l'evento",
|
||||
"compose_event.tabs.edit": "Modifica i dettagli",
|
||||
"compose_event.tabs.pending": "Gestisci le richieste",
|
||||
"compose_event.update": "Aggiorna",
|
||||
"compose_form.direct_message_warning": "Stai scrivendo solo alle persone menzionate, ma ricorda che gli amministratori delle istanze coinvolte potrebbero comunque leggere il messaggio.",
|
||||
"compose_form.event_placeholder": "Pubblica nell'evento",
|
||||
"compose_form.hashtag_warning": "Pubblicazione non elencata, sarà impossibile trovarla nelle ricerche anche indicando gli hashtag. Le pubblicazioni possono essere cercate soltanto impostando la visibilità a livello «Pubblico».",
|
||||
"compose_form.lock_disclaimer": "Il tuo profilo non è {locked}. Chiunque può decidere di seguirti, anche solo per vedere le pubblicazioni per sole persone Follower.",
|
||||
"compose_form.lock_disclaimer.lock": "protetto da approvazione",
|
||||
|
@ -330,15 +429,22 @@
|
|||
"confirmations.cancel_editing.confirm": "Annulla modifiche",
|
||||
"confirmations.cancel_editing.heading": "Interrompi la pubblicazione di modifiche",
|
||||
"confirmations.cancel_editing.message": "Vuoi davvero annullare la modifica di questa pubblicazione? I cambiamenti verranno persi.",
|
||||
"confirmations.cancel_event_editing.heading": "Annulla le modifiche",
|
||||
"confirmations.cancel_event_editing.message": "Vuoi davvero smettere di modificare l'evento? Perderai i cambiamenti.",
|
||||
"confirmations.delete.confirm": "Elimina",
|
||||
"confirmations.delete.heading": "Elimina pubblicazione",
|
||||
"confirmations.delete.message": "Vuoi davvero eliminare questa pubblicazione?",
|
||||
"confirmations.delete_event.confirm": "Elimina",
|
||||
"confirmations.delete_event.heading": "Elimina l'evento",
|
||||
"confirmations.delete_event.message": "Vuoi davvero eliminare questo evento?",
|
||||
"confirmations.delete_list.confirm": "Elimina",
|
||||
"confirmations.delete_list.heading": "Elimina lista",
|
||||
"confirmations.delete_list.message": "Vuoi davvero eliminare questa lista?",
|
||||
"confirmations.domain_block.confirm": "Nascondi intero dominio",
|
||||
"confirmations.domain_block.heading": "Block {domain}",
|
||||
"confirmations.domain_block.message": "Vuoi davvero bloccare l'intero {domain}? Nella maggior parte dei casi, pochi blocchi o silenziamenti mirati sono sufficienti e preferibili. Non vedrai nessuna pubblicazione di quel dominio né nelle timeline pubbliche né nelle notifiche. I tuoi seguaci di quel dominio saranno eliminati.",
|
||||
"confirmations.leave_event.confirm": "Abbandona",
|
||||
"confirmations.leave_event.message": "Se vorrai partecipare nuovamente, la tua richiesta dovrà essere riconfermata. Vuoi davvero procedere?",
|
||||
"confirmations.mute.confirm": "Silenzia",
|
||||
"confirmations.mute.heading": "Silenzia @{name}",
|
||||
"confirmations.mute.message": "Vuoi davvero silenziare {name}?",
|
||||
|
@ -440,7 +546,7 @@
|
|||
"edit_profile.success": "Profilo aggiornato!",
|
||||
"email_confirmation.success": "L'indirizzo email è stato confermato!",
|
||||
"email_passthru.confirmed.body": "Chiudi questo Tab e continua la procedura di registrazione su {bold} a cui hai spedito questa conferma email.",
|
||||
"email_passthru.confirmed.heading": "Indirizzo e-mail confermato.",
|
||||
"email_passthru.confirmed.heading": "E-mail confermata!",
|
||||
"email_passthru.fail.expired": "Il tuo token e-mail è scaduto",
|
||||
"email_passthru.fail.generic": "Impossibile confermare la tua email",
|
||||
"email_passthru.fail.invalid_token": "Il tuo Token non è valido",
|
||||
|
|
|
@ -154,7 +154,7 @@
|
|||
"aliases.success.add": "Account alias created successfully",
|
||||
"aliases.success.remove": "Account alias removed successfully",
|
||||
"announcements.title": "Announcements",
|
||||
"app_create.name_label": "Назва додатку",
|
||||
"app_create.name_label": "Назва застосунку",
|
||||
"app_create.name_placeholder": "e.g. 'Soapbox'",
|
||||
"app_create.redirect_uri_label": "URI перенаправлення",
|
||||
"app_create.restart": "Create another",
|
||||
|
@ -551,7 +551,7 @@
|
|||
"gdpr.learn_more": "Learn more",
|
||||
"gdpr.message": "{siteTitle} uses session cookies, which are essential to the website's functioning.",
|
||||
"gdpr.title": "{siteTitle} uses cookies",
|
||||
"getting_started.open_source_notice": "{code_name} — програма з відкритим сирцевим кодом. Ви можете допомогти проекту, або повідомити про проблеми на GitLab за адресою {code_link} (v{code_version}).",
|
||||
"getting_started.open_source_notice": "{code_name} — програмне забезпечення з відкритим кодом. Ви можете допомогти проєкту, або повідомити про проблеми на GitLab за адресою {code_link} (v{code_version}).",
|
||||
"hashtag.column_header.tag_mode.all": "та {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "або {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "без {additional}",
|
||||
|
|
|
@ -8,7 +8,11 @@ import {
|
|||
|
||||
import LinkFooter from '../features/ui/components/link-footer';
|
||||
|
||||
const AdminPage: React.FC = ({ children }) => {
|
||||
interface IAdminPage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AdminPage: React.FC<IAdminPage> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Layout.Main>
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
interface IChatsPage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
/** Custom layout for chats on desktop. */
|
||||
const ChatsPage: React.FC = ({ children }) => {
|
||||
const ChatsPage: React.FC<IChatsPage> = ({ children }) => {
|
||||
return (
|
||||
<div className='md:col-span-12 lg:col-span-9'>
|
||||
{children}
|
||||
|
|
|
@ -12,7 +12,11 @@ import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
|||
|
||||
import { Layout } from '../components/ui';
|
||||
|
||||
const DefaultPage: React.FC = ({ children }) => {
|
||||
interface IDefaultPage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const DefaultPage: React.FC<IDefaultPage> = ({ children }) => {
|
||||
const me = useAppSelector(state => state.me);
|
||||
const features = useFeatures();
|
||||
|
||||
|
|
|
@ -2,7 +2,11 @@ import React from 'react';
|
|||
|
||||
import { Layout } from '../components/ui';
|
||||
|
||||
const EmptyPage: React.FC = ({ children }) => {
|
||||
interface IEmptyPage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const EmptyPage: React.FC<IEmptyPage> = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Layout.Main>
|
||||
|
|
|
@ -21,6 +21,7 @@ interface IEventPage {
|
|||
params?: {
|
||||
statusId?: string,
|
||||
},
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const EventPage: React.FC<IEventPage> = ({ params, children }) => {
|
||||
|
|
|
@ -20,7 +20,11 @@ import { Avatar, Card, CardBody, HStack, Layout } from '../components/ui';
|
|||
import ComposeForm from '../features/compose/components/compose-form';
|
||||
import BundleContainer from '../features/ui/containers/bundle-container';
|
||||
|
||||
const HomePage: React.FC = ({ children }) => {
|
||||
interface IHomePage {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const HomePage: React.FC<IHomePage> = ({ children }) => {
|
||||
const me = useAppSelector(state => state.me);
|
||||
const account = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
|
|
|
@ -23,6 +23,7 @@ interface IProfilePage {
|
|||
params?: {
|
||||
username?: string,
|
||||
},
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const getAccount = makeGetAccount();
|
||||
|
|
|
@ -16,6 +16,7 @@ interface IRemoteInstancePage {
|
|||
params?: {
|
||||
instance?: string,
|
||||
},
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
/** Page for viewing a remote instance timeline. */
|
||||
|
|
|
@ -178,6 +178,13 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
*/
|
||||
announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
|
||||
|
||||
/**
|
||||
* Pleroma backups.
|
||||
* @see GET /api/v1/pleroma/backups
|
||||
* @see POST /api/v1/pleroma/backups
|
||||
*/
|
||||
backups: v.software === PLEROMA,
|
||||
|
||||
/**
|
||||
* Set your birthday and view upcoming birthdays.
|
||||
* @see GET /api/v1/pleroma/birthdays
|
||||
|
@ -381,6 +388,9 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
v.software === TRUTHSOCIAL,
|
||||
]),
|
||||
|
||||
/** Whether to allow exporting follows/blocks/mutes to CSV by paginating the API. */
|
||||
exportData: true,
|
||||
|
||||
/** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
|
||||
exposableReactions: any([
|
||||
v.software === MASTODON,
|
||||
|
|
|
@ -43,7 +43,6 @@
|
|||
@import 'components/video-player';
|
||||
@import 'components/audio-player';
|
||||
@import 'components/filters';
|
||||
@import 'components/backups';
|
||||
@import 'components/crypto-donate';
|
||||
@import 'components/aliases';
|
||||
@import 'components/icon';
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
.backup {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--brand-color--faint);
|
||||
|
||||
a {
|
||||
color: var(--brand-color--hicontrast);
|
||||
}
|
||||
|
||||
&--pending {
|
||||
@apply text-gray-400 italic;
|
||||
}
|
||||
}
|
|
@ -79,9 +79,10 @@
|
|||
"@types/leaflet": "^1.8.0",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/object-assign": "^4.0.30",
|
||||
"@types/qrcode.react": "^1.0.2",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-datepicker": "^4.4.2",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/react-helmet": "^6.1.5",
|
||||
"@types/react-motion": "^0.0.33",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
|
@ -142,7 +143,7 @@
|
|||
"postcss-loader": "^7.0.0",
|
||||
"process": "^0.11.10",
|
||||
"punycode": "^2.1.1",
|
||||
"qrcode.react": "^3.0.2",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.0.0",
|
||||
"react-color": "^2.19.3",
|
||||
"react-datepicker": "^4.8.0",
|
||||
|
@ -233,6 +234,8 @@
|
|||
"yargs": "^17.6.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"glob-parent": "^6.0.1",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"loader-utils": "^2.0.3"
|
||||
|
|
31
yarn.lock
31
yarn.lock
|
@ -2815,13 +2815,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11"
|
||||
integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==
|
||||
|
||||
"@types/qrcode.react@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/qrcode.react/-/qrcode.react-1.0.2.tgz#f892432cc41b5dac52e3ca8873b717c8bfea6002"
|
||||
integrity sha512-I9Oq5Cjlkgy3Tw7krCnCXLw2/zMhizkTere49OOcta23tkvH0xBTP0yInimTh0gstLRtb8Ki9NZVujE5UI6ffQ==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
|
||||
|
@ -2850,7 +2843,7 @@
|
|||
date-fns "^2.0.1"
|
||||
react-popper "^2.2.5"
|
||||
|
||||
"@types/react-dom@^18.0.0":
|
||||
"@types/react-dom@^18.0.0", "@types/react-dom@^18.0.10":
|
||||
version "18.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.10.tgz#3b66dec56aa0f16a6cc26da9e9ca96c35c0b4352"
|
||||
integrity sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==
|
||||
|
@ -2881,9 +2874,9 @@
|
|||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router@*":
|
||||
version "5.1.18"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.18.tgz#c8851884b60bc23733500d86c1266e1cfbbd9ef3"
|
||||
integrity sha512-YYknwy0D0iOwKQgz9v8nOzt2J6l4gouBmDnWqUUznltOTaon+r8US8ky8HvN0tXvc38U9m6z/t2RsVsnd1zM0g==
|
||||
version "5.1.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.20.tgz#88eccaa122a82405ef3efbcaaa5dcdd9f021387c"
|
||||
integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
|
||||
dependencies:
|
||||
"@types/history" "^4.7.11"
|
||||
"@types/react" "*"
|
||||
|
@ -2909,10 +2902,10 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@17":
|
||||
version "17.0.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.21.tgz#069c43177cd419afaab5ce26bb4e9056549f7ea6"
|
||||
integrity sha512-GzzXCpOthOjXvrAUFQwU/svyxu658cwu00Q9ugujS4qc1zXgLFaO0kS2SLOaMWLt2Jik781yuHCWB7UcYdGAeQ==
|
||||
"@types/react@*", "@types/react@17", "@types/react@^18.0.26":
|
||||
version "18.0.26"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.26.tgz#8ad59fc01fef8eaf5c74f4ea392621749f0b7917"
|
||||
integrity sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
|
@ -9266,10 +9259,10 @@ punycode@^2.1.0, punycode@^2.1.1:
|
|||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
qrcode.react@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.0.2.tgz#7ceaea165aa7066253ef670a25bf238eaec4eb9e"
|
||||
integrity sha512-8F3SGxSkNb3fMIHdlseqjFjLbsPrF3WvF/1MOboSUUHytT537W8f/FtbdA3XFIHDrc+TrRBjTI/QLmwhAIGWWw==
|
||||
qrcode.react@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
|
||||
integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
|
||||
|
||||
qs@6.10.3:
|
||||
version "6.10.3"
|
||||
|
|
Loading…
Reference in a new issue