Merge remote-tracking branch 'origin/develop' into chats
This commit is contained in:
commit
5bb30b6282
146 changed files with 943 additions and 1140 deletions
|
@ -1,6 +1,5 @@
|
|||
# Custom icons
|
||||
|
||||
- fediverse.svg - Modified from Wikipedia, CC0
|
||||
- verified.svg - Created by Alex Gleason. CC0
|
||||
|
||||
Fediverse logo: https://en.wikipedia.org/wiki/Fediverse#/media/File:Fediverse_logo_proposal.svg
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1792" height="1792" viewBox="0 0 1792 1792" fill="currentColor">
|
||||
<path d="M 343.16176,591.34767 A 171.09965,171.09965 0 0 1 163.0094,752.88799 171.09965,171.09965 0 0 1 1.4690156,572.73567 171.09965,171.09965 0 0 1 181.62138,411.19523 171.09965,171.09965 0 0 1 343.16176,591.34767 Z M 482.96755,1485.6494 A 171.09965,171.09965 0 0 1 302.81519,1647.1895 171.09965,171.09965 0 0 1 141.2748,1467.0372 171.09965,171.09965 0 0 1 321.42717,1305.4967 171.09965,171.09965 0 0 1 482.96755,1485.6494 Z m 893.94285,143.4473 a 171.09965,171.09965 0 0 1 -180.1523,161.5405 171.09965,171.09965 0 0 1 -161.5404,-180.1524 171.09965,171.09965 0 0 1 180.1524,-161.5405 171.09965,171.09965 0 0 1 161.5403,180.1524 z M 1789.5918,823.45087 A 171.09965,171.09965 0 0 1 1609.4395,984.99118 171.09965,171.09965 0 0 1 1447.899,804.83886 171.09965,171.09965 0 0 1 1628.0513,643.29854 171.09965,171.09965 0 0 1 1789.5918,823.45087 Z M 1150.7,182.08661 A 171.09965,171.09965 0 0 1 970.54747,343.627 171.09965,171.09965 0 0 1 809.00715,163.47462 171.09965,171.09965 0 0 1 989.15948,1.9342312 171.09965,171.09965 0 0 1 1150.7,182.08661 Z m -792.52346,371.6819 a 188.20963,188.20963 0 0 1 2.07029,38.5086 188.20963,188.20963 0 0 1 -19.56107,73.71432 l 276.93395,44.47923 54.4306,-106.2947 z m 474.63645,76.2221 -54.43595,106.30538 654.33596,105.0888 a 188.20963,188.20963 0 0 1 -1.8996,-37.47876 188.20963,188.20963 0 0 1 20.0788,-74.64799 z M 1065.1875,340.27205 a 188.20963,188.20963 0 0 1 -95.56964,20.44149 188.20963,188.20963 0 0 1 -16.47175,-1.72883 l 43.21482,276.72059 117.91607,18.92069 z m -43.7109,456.30786 102.1755,654.25059 a 188.20963,188.20963 0 0 1 92.651,-18.9688 188.20963,188.20963 0 0 1 19.6891,2.1609 L 1139.398,815.49535 Z M 794.34712,203.1417 306.48853,450.37661 a 188.20963,188.20963 0 0 1 51.34118,101.31626 L 845.683,304.44741 A 188.20963,188.20963 0 0 1 794.34712,203.1417 Z m 352.24368,56.54894 a 188.20963,188.20963 0 0 1 -80.5121,80.13321 l 385.9286,387.41724 a 188.20963,188.20963 0 0 1 80.5069,-80.13321 z m 339.8804,688.09027 -249.2037,486.50849 a 188.20963,188.20963 0 0 1 101.0656,51.8588 L 1587.5315,999.64494 A 188.20963,188.20963 0 0 1 1486.4712,947.78091 Z M 498.08153,1448.6696 a 188.20963,188.20963 0 0 1 1.9689,37.9111 188.20963,188.20963 0 0 1 -19.85455,74.2528 l 539.90952,86.6376 a 188.20963,188.20963 0 0 1 -1.9742,-37.9162 188.20963,188.20963 0 0 1 19.8598,-74.2477 z M 256.10246,750.31321 a 188.20963,188.20963 0 0 1 -94.02233,19.65712 188.20963,188.20963 0 0 1 -18.16843,-1.89976 l 84.42322,540.00023 a 188.20963,188.20963 0 0 1 94.02233,-19.6571 188.20963,188.20963 0 0 1 18.15773,1.9001 z M 847.58784,306.427 562.15394,863.6618 646.42776,948.26106 948.64274,358.28043 A 188.20963,188.20963 0 0 1 847.58784,306.427 Z m -359.67106,702.1662 -144.57913,282.2484 a 188.20963,188.20963 0 0 1 101.04426,51.8481 l 127.80337,-249.5025 z m 945.31912,-164.10293 -250.1803,126.78949 18.446,117.99084 283.07,-143.4639 A 188.20963,188.20963 0 0 1 1433.2359,844.49027 Z M 1037.8202,1044.882 446.28679,1344.6691 a 188.20963,188.20963 0 0 1 51.34651,101.3273 l 558.6328,-283.1181 z M 339.05298,668.95274 a 188.20963,188.20963 0 0 1 -80.49604,80.12252 l 441.91192,443.63544 106.54017,-53.9931 z m 582.89469,585.14656 -106.54012,53.993 223.91735,224.7922 a 188.20963,188.20963 0 0 1 80.5121,-80.1332 z" fill-opacity=".996"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 3.3 KiB |
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface IInlineSVG {
|
||||
loader?: JSX.Element,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
|
@ -199,7 +199,7 @@ const Account = ({
|
|||
title={account.acct}
|
||||
onClick={(event: React.MouseEvent) => event.stopPropagation()}
|
||||
>
|
||||
<div className='flex items-center space-x-1 flex-grow' style={style}>
|
||||
<HStack space={1} alignItems='center' grow style={style}>
|
||||
<Text
|
||||
size='sm'
|
||||
weight='semibold'
|
||||
|
@ -208,7 +208,7 @@ const Account = ({
|
|||
/>
|
||||
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
</HStack>
|
||||
</LinkEl>
|
||||
</ProfilePopper>
|
||||
|
||||
|
@ -255,7 +255,7 @@ const Account = ({
|
|||
<Text
|
||||
size='sm'
|
||||
dangerouslySetInnerHTML={{ __html: account.note_emojified }}
|
||||
className='mr-2'
|
||||
className='mr-2 rtl:ml-2 rtl:mr-0'
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -28,7 +28,7 @@ const CopyableInput: React.FC<ICopyableInput> = ({ value }) => {
|
|||
ref={input}
|
||||
type='text'
|
||||
value={value}
|
||||
className='rounded-r-none'
|
||||
className='rounded-r-none rtl:rounded-l-none rtl:rounded-r-lg'
|
||||
outerClassName='flex-grow'
|
||||
onClick={selectInput}
|
||||
readOnly
|
||||
|
@ -36,7 +36,7 @@ const CopyableInput: React.FC<ICopyableInput> = ({ value }) => {
|
|||
|
||||
<Button
|
||||
theme='primary'
|
||||
className='mt-1 h-full rounded-l-none rounded-r-lg'
|
||||
className='mt-1 h-full rounded-l-none rounded-r-lg rtl:rounded-l-lg rtl:rounded-r-none'
|
||||
onClick={selectInput}
|
||||
>
|
||||
<FormattedMessage id='input.copy' defaultMessage='Copy' />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import HoverRefWrapper from 'soapbox/components/hover-ref-wrapper';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
@ -7,16 +7,18 @@ import { getAcct } from '../utils/accounts';
|
|||
|
||||
import Icon from './icon';
|
||||
import RelativeTimestamp from './relative-timestamp';
|
||||
import { HStack, Text } from './ui';
|
||||
import VerificationBadge from './verification-badge';
|
||||
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
interface IDisplayName {
|
||||
account: Account
|
||||
withSuffix?: boolean
|
||||
withDate?: boolean
|
||||
}
|
||||
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withDate = false }) => {
|
||||
const DisplayName: React.FC<IDisplayName> = ({ account, children, withSuffix = true, withDate = false }) => {
|
||||
const { displayFqn = false } = useSoapboxConfig();
|
||||
const { created_at: createdAt, verified } = account;
|
||||
|
||||
|
@ -28,11 +30,17 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withDate = fal
|
|||
) : null;
|
||||
|
||||
const displayName = (
|
||||
<span className='display-name__name'>
|
||||
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi>
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
<Text
|
||||
size='sm'
|
||||
weight='semibold'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
|
||||
{verified && <VerificationBadge />}
|
||||
{withDate && joinedAt}
|
||||
</span>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
const suffix = (<span className='display-name__account'>@{getAcct(account, displayFqn)}</span>);
|
||||
|
@ -42,7 +50,7 @@ const DisplayName: React.FC<IDisplayName> = ({ account, children, withDate = fal
|
|||
<HoverRefWrapper accountId={account.get('id')} inline>
|
||||
{displayName}
|
||||
</HoverRefWrapper>
|
||||
{suffix}
|
||||
{withSuffix && suffix}
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { spring } from 'react-motion';
|
|||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import { withRouter, RouteComponentProps } from 'react-router-dom';
|
||||
|
||||
import { IconButton, Counter } from 'soapbox/components/ui';
|
||||
import { Counter, IconButton } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||
|
||||
|
@ -196,7 +196,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
data-method={isLogout ? 'delete' : undefined}
|
||||
title={text}
|
||||
>
|
||||
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none' />}
|
||||
{icon && <SvgIcon src={icon} className='mr-3 rtl:ml-3 rtl:mr-0 h-5 w-5 flex-none' />}
|
||||
|
||||
<span className='truncate'>{text}</span>
|
||||
|
||||
|
@ -264,6 +264,7 @@ export interface IDropdown extends RouteComponentProps {
|
|||
text?: string,
|
||||
onShiftClick?: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||
children?: JSX.Element,
|
||||
dropdownMenuStyle?: React.CSSProperties,
|
||||
}
|
||||
|
||||
interface IDropdownState {
|
||||
|
@ -374,7 +375,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children } = this.props;
|
||||
const { src = require('@tabler/icons/dots.svg'), items, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard = false, pressed, text, children, dropdownMenuStyle } = this.props;
|
||||
const open = this.state.id === openDropdownId;
|
||||
|
||||
return (
|
||||
|
@ -408,7 +409,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
)}
|
||||
|
||||
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
|
||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
|
||||
<RouterDropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} style={dropdownMenuStyle} />
|
||||
</Overlay>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useDispatch } from 'react-redux';
|
|||
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import EmojiSelector from 'soapbox/components/ui/emoji-selector/emoji-selector';
|
||||
import { EmojiSelector } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useOwnAccount, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
|
|
@ -4,7 +4,7 @@ import { connect } from 'react-redux';
|
|||
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import * as BuildConfig from 'soapbox/build-config';
|
||||
import { Text, Stack } from 'soapbox/components/ui';
|
||||
import { HStack, Text, Stack } from 'soapbox/components/ui';
|
||||
import { captureException } from 'soapbox/monitoring';
|
||||
import KVStore from 'soapbox/storage/kv-store';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
@ -179,7 +179,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</main>
|
||||
|
||||
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<nav className='flex justify-center space-x-4'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
|
@ -205,7 +205,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</a>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</HStack>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -31,7 +31,7 @@ const GdprBanner: React.FC = () => {
|
|||
|
||||
return (
|
||||
<Banner theme='opaque' className={classNames('transition-transform', { 'translate-y-full': slideout })}>
|
||||
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 lg:flex-row lg:items-center lg:justify-between'>
|
||||
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between'>
|
||||
<Stack space={2}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle }} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Helmet as ReactHelmet } from 'react-helmet';
|
||||
|
||||
import { useAppSelector, useSettings } from 'soapbox/hooks';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { SelectDropdown } from '../features/forms';
|
||||
|
||||
import Icon from './icon';
|
||||
import { Select } from './ui';
|
||||
import { HStack, Select } from './ui';
|
||||
|
||||
const List: React.FC = ({ children }) => (
|
||||
<div className='space-y-0.5'>{children}</div>
|
||||
|
@ -23,9 +23,15 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
const id = uuidv4();
|
||||
const domId = `list-group-${id}`;
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
onClick!();
|
||||
}
|
||||
};
|
||||
|
||||
const Comp = onClick ? 'a' : 'div';
|
||||
const LabelComp = onClick || onSelect ? 'span' : 'label';
|
||||
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect } : {};
|
||||
const linkProps = onClick || onSelect ? { onClick: onClick || onSelect, onKeyDown, tabIndex: 0, role: 'link' } : {};
|
||||
|
||||
const renderChildren = React.useCallback(() => {
|
||||
return React.Children.map(children, (child) => {
|
||||
|
@ -52,7 +58,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
})}
|
||||
{...linkProps}
|
||||
>
|
||||
<div className='flex flex-col py-1.5 pr-4'>
|
||||
<div className='flex flex-col py-1.5 pr-4 rtl:pl-4 rtl:pr-0'>
|
||||
<LabelComp className='text-gray-900 dark:text-gray-100' htmlFor={domId}>{label}</LabelComp>
|
||||
|
||||
{hint ? (
|
||||
|
@ -61,11 +67,11 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
</div>
|
||||
|
||||
{onClick ? (
|
||||
<div className='flex flex-row items-center text-gray-700 dark:text-gray-600'>
|
||||
<HStack space={1} alignItems='center' className='text-gray-700 dark:text-gray-600'>
|
||||
{children}
|
||||
|
||||
<Icon src={require('@tabler/icons/chevron-right.svg')} className='ml-1' />
|
||||
</div>
|
||||
</HStack>
|
||||
) : null}
|
||||
|
||||
{onSelect ? (
|
||||
|
|
Binary file not shown.
|
@ -160,16 +160,22 @@ const Item: React.FC<IItem> = ({
|
|||
</div>
|
||||
);
|
||||
} else if (attachment.type === 'image') {
|
||||
const letterboxed = shouldLetterbox(attachment);
|
||||
const letterboxed = total === 1 && shouldLetterbox(attachment);
|
||||
|
||||
thumbnail = (
|
||||
<a
|
||||
className={classNames('media-gallery__item-thumbnail', { letterboxed })}
|
||||
className='media-gallery__item-thumbnail'
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
>
|
||||
<StillImage src={attachment.url} alt={attachment.description} />
|
||||
<StillImage
|
||||
className='w-full h-full'
|
||||
src={attachment.url}
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
showExt
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else if (attachment.type === 'gifv') {
|
||||
|
|
|
@ -106,7 +106,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
onMouseEnter={handleMouseEnter(dispatch)}
|
||||
onMouseLeave={handleMouseLeave(dispatch)}
|
||||
>
|
||||
<Card variant='rounded' className='relative'>
|
||||
<Card variant='rounded' className='relative isolate'>
|
||||
<CardBody>
|
||||
<Stack space={2}>
|
||||
<BundleContainer fetchComponent={UserPanel}>
|
||||
|
@ -136,7 +136,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
</HStack>
|
||||
) : null}
|
||||
|
||||
{account.source.get('note', '').length > 0 && (
|
||||
{account.note.length > 0 && (
|
||||
<Text size='sm' dangerouslySetInnerHTML={accountBio} />
|
||||
)}
|
||||
</Stack>
|
||||
|
|
|
@ -38,6 +38,7 @@ const messages = defineMessages({
|
|||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
addAccount: { id: 'profile_dropdown.add_account', defaultMessage: 'Add an existing account' },
|
||||
followRequests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
interface ISidebarLink {
|
||||
|
@ -82,7 +83,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
const features = useFeatures();
|
||||
const getAccount = makeGetAccount();
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const me = useAppSelector((state) => state.me);
|
||||
const account = useAppSelector((state) => me ? getAccount(state, me) : null);
|
||||
const otherAccounts: ImmutableList<AccountEntity> = useAppSelector((state) => getOtherAccounts(state));
|
||||
|
@ -134,9 +134,11 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div className={classNames('sidebar-menu__root', {
|
||||
'sidebar-menu__root--visible': sidebarOpen,
|
||||
})}
|
||||
<div
|
||||
className={classNames('sidebar-menu__root', {
|
||||
'sidebar-menu__root--visible': sidebarOpen,
|
||||
})}
|
||||
aria-expanded={sidebarOpen}
|
||||
>
|
||||
<div
|
||||
className={classNames({
|
||||
|
@ -147,7 +149,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
onClick={handleClose}
|
||||
>
|
||||
<IconButton
|
||||
title='close'
|
||||
title={intl.formatMessage(messages.close)}
|
||||
onClick={handleClose}
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
ref={closeButtonRef}
|
||||
|
@ -220,15 +222,15 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
<SidebarLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/users.svg') : require('@tabler/icons/world.svg')}
|
||||
text={features.federating ? instance.title : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
icon={features.federating ? require('@tabler/icons/affiliate.svg') : require('@tabler/icons/world.svg')}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
<SidebarLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('assets/icons/fediverse.svg')}
|
||||
icon={require('@tabler/icons/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ISidebarNavigationLink {
|
|||
/** URL to an SVG icon. */
|
||||
icon: string,
|
||||
/** Link label. */
|
||||
text: React.ReactElement,
|
||||
text: React.ReactNode,
|
||||
/** Route to an internal page. */
|
||||
to?: string,
|
||||
/** Callback when the link is clicked. */
|
||||
|
@ -39,7 +39,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
|||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={classNames({
|
||||
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true,
|
||||
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rtl:space-x-reverse rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true,
|
||||
'dark:text-gray-100 text-gray-900': isActive,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -17,9 +17,6 @@ const messages = defineMessages({
|
|||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
dashboard: { id: 'tabs_bar.dashboard', defaultMessage: 'Dashboard' },
|
||||
all: { id: 'tabs_bar.all', defaultMessage: 'All' },
|
||||
fediverse: { id: 'tabs_bar.fediverse', defaultMessage: 'Fediverse' },
|
||||
});
|
||||
|
||||
/** Desktop sidebar with links to different views in the app. */
|
||||
|
@ -72,35 +69,6 @@ const SidebarNavigation = () => {
|
|||
text: intl.formatMessage(messages.developers),
|
||||
});
|
||||
}
|
||||
|
||||
if (account.staff) {
|
||||
menu.push({
|
||||
to: '/soapbox/admin',
|
||||
icon: require('@tabler/icons/dashboard.svg'),
|
||||
text: intl.formatMessage(messages.dashboard),
|
||||
count: dashboardCount,
|
||||
});
|
||||
}
|
||||
|
||||
if (features.publicTimeline) {
|
||||
menu.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (features.publicTimeline) {
|
||||
menu.push({
|
||||
to: '/timeline/local',
|
||||
icon: features.federating ? require('@tabler/icons/users.svg') : require('@tabler/icons/world.svg'),
|
||||
text: features.federating ? instance.title : intl.formatMessage(messages.all),
|
||||
});
|
||||
}
|
||||
|
||||
if (features.publicTimeline && features.federating) {
|
||||
menu.push({
|
||||
to: '/timeline/fediverse',
|
||||
icon: require('assets/icons/fediverse.svg'),
|
||||
text: intl.formatMessage(messages.fediverse),
|
||||
});
|
||||
}
|
||||
|
||||
return menu;
|
||||
|
@ -172,6 +140,33 @@ const SidebarNavigation = () => {
|
|||
icon={require('@tabler/icons/settings.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.settings' defaultMessage='Settings' />}
|
||||
/>
|
||||
|
||||
{account.staff && (
|
||||
<SidebarNavigationLink
|
||||
to='/soapbox/admin'
|
||||
icon={require('@tabler/icons/dashboard.svg')}
|
||||
count={dashboardCount}
|
||||
text={<FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{features.publicTimeline && (
|
||||
<>
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/local'
|
||||
icon={features.federating ? require('@tabler/icons/affiliate.svg') : require('@tabler/icons/world.svg')}
|
||||
text={features.federating ? <FormattedMessage id='tabs_bar.local' defaultMessage='Local' /> : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />}
|
||||
/>
|
||||
|
||||
{features.federating && (
|
||||
<SidebarNavigationLink
|
||||
to='/timeline/fediverse'
|
||||
icon={require('@tabler/icons/topology-star-ring-3.svg')}
|
||||
text={<FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
@ -179,7 +174,6 @@ const SidebarNavigation = () => {
|
|||
<DropdownMenu items={menu}>
|
||||
<SidebarNavigationLink
|
||||
icon={require('@tabler/icons/dots-circle-horizontal.svg')}
|
||||
count={dashboardCount}
|
||||
text={<FormattedMessage id='tabs_bar.more' defaultMessage='More' />}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
|
|
|
@ -14,7 +14,9 @@ interface ISiteLogo extends React.ComponentProps<'img'> {
|
|||
const SiteLogo: React.FC<ISiteLogo> = ({ className, theme, ...rest }) => {
|
||||
const { logo, logoDarkMode } = useSoapboxConfig();
|
||||
const settings = useSettings();
|
||||
const darkMode = useTheme() === 'dark';
|
||||
|
||||
let darkMode = useTheme() === 'dark';
|
||||
if (theme === 'dark') darkMode = true;
|
||||
|
||||
/** Soapbox logo. */
|
||||
const soapboxLogo = darkMode
|
||||
|
|
|
@ -18,7 +18,7 @@ import StatusActionButton from 'soapbox/components/status-action-button';
|
|||
import { HStack } from 'soapbox/components/ui';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
import { isLocal, isRemote } from 'soapbox/utils/accounts';
|
||||
import { getReactForStatus, reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
import type { Menu } from 'soapbox/components/dropdown-menu';
|
||||
|
@ -56,6 +56,7 @@ const messages = defineMessages({
|
|||
copy: { id: 'status.copy', defaultMessage: 'Copy link to post' },
|
||||
group_remove_account: { id: 'status.remove_account_from_group', defaultMessage: 'Remove account from group' },
|
||||
group_remove_post: { id: 'status.remove_post_from_group', defaultMessage: 'Remove post from group' },
|
||||
external: { id: 'status.external', defaultMessage: 'View post on {domain}' },
|
||||
deactivateUser: { id: 'admin.users.actions.deactivate_user', defaultMessage: 'Deactivate @{name}' },
|
||||
deleteUser: { id: 'admin.users.actions.delete_user', defaultMessage: 'Delete @{name}' },
|
||||
deleteStatus: { id: 'admin.statuses.actions.delete_status', defaultMessage: 'Delete post' },
|
||||
|
@ -150,6 +151,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
dispatch(toggleBookmark(status));
|
||||
};
|
||||
|
||||
const handleExternalClick = () => {
|
||||
window.open(status.uri, '_blank');
|
||||
};
|
||||
|
||||
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
|
||||
if (me) {
|
||||
const modalReblog = () => dispatch(toggleReblog(status));
|
||||
|
@ -294,6 +299,8 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
const mutingConversation = status.muted;
|
||||
const ownAccount = status.getIn(['account', 'id']) === me;
|
||||
const username = String(status.getIn(['account', 'username']));
|
||||
const account = status.account as Account;
|
||||
const domain = account.fqn.split('@')[1];
|
||||
|
||||
const menu: Menu = [];
|
||||
|
||||
|
@ -312,7 +319,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
icon: require('@tabler/icons/link.svg'),
|
||||
});
|
||||
|
||||
if (features.embeds && isLocal(status.account as Account)) {
|
||||
if (features.embeds && isLocal(account)) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.embed),
|
||||
action: handleEmbed,
|
||||
|
@ -333,6 +340,14 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
});
|
||||
}
|
||||
|
||||
if (features.federating && isRemote(account)) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(messages.external, { domain }),
|
||||
action: handleExternalClick,
|
||||
icon: require('@tabler/icons/external-link.svg'),
|
||||
});
|
||||
}
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (ownAccount || withDismiss) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
|||
import { addAutoPlay } from 'soapbox/utils/media';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type VideoType from 'soapbox/features/video';
|
||||
import type { Status, Attachment } from 'soapbox/types/entities';
|
||||
|
||||
interface IStatusMedia {
|
||||
|
@ -66,10 +67,6 @@ const StatusMedia: React.FC<IStatusMedia> = ({
|
|||
dispatch(openModal('MEDIA', { media, status, index }));
|
||||
};
|
||||
|
||||
const openVideo = (media: Attachment, time: number): void => {
|
||||
dispatch(openModal('VIDEO', { media, time }));
|
||||
};
|
||||
|
||||
if (size > 0 && firstAttachment) {
|
||||
if (muted) {
|
||||
media = (
|
||||
|
@ -105,20 +102,17 @@ const StatusMedia: React.FC<IStatusMedia> = ({
|
|||
);
|
||||
} else {
|
||||
media = (
|
||||
<Bundle fetchComponent={Video} loading={renderLoadingVideoPlayer} >
|
||||
{(Component: any) => (
|
||||
<Bundle fetchComponent={Video} loading={renderLoadingVideoPlayer}>
|
||||
{(Component: typeof VideoType) => (
|
||||
<Component
|
||||
preview={video.preview_url}
|
||||
blurhash={video.blurhash}
|
||||
src={video.url}
|
||||
alt={video.description}
|
||||
aspectRatio={video.meta.getIn(['original', 'aspect'])}
|
||||
aspectRatio={Number(video.meta.getIn(['original', 'aspect']))}
|
||||
height={285}
|
||||
inline
|
||||
sensitive={status.sensitive}
|
||||
onOpenVideo={openVideo}
|
||||
visible={showMedia}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
inline
|
||||
/>
|
||||
)}
|
||||
</Bundle>
|
||||
|
|
|
@ -66,7 +66,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
|
||||
if (to.size > 2) {
|
||||
accounts.push(
|
||||
<span key='more' className='hover:underline cursor-pointer' role='presentation' onClick={handleOpenMentionsModal}>
|
||||
<span key='more' className='hover:underline cursor-pointer' role='button' onClick={handleOpenMentionsModal} tabIndex={0}>
|
||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />
|
||||
</span>,
|
||||
);
|
||||
|
|
|
@ -12,10 +12,14 @@ interface IStillImage {
|
|||
src: string,
|
||||
/** Extra CSS styles on the outer <div> element. */
|
||||
style?: React.CSSProperties,
|
||||
/** Whether to display the image contained vs filled in its container. */
|
||||
letterboxed?: boolean,
|
||||
/** Whether to show the file extension in the corner. */
|
||||
showExt?: boolean,
|
||||
}
|
||||
|
||||
/** Renders images on a canvas, only playing GIFs if autoPlayGif is enabled. */
|
||||
const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
||||
const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterboxed = false, showExt = false }) => {
|
||||
const settings = useSettings();
|
||||
const autoPlayGif = settings.get('autoPlayGif');
|
||||
|
||||
|
@ -34,10 +38,56 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style }) => {
|
|||
}
|
||||
};
|
||||
|
||||
/** ClassNames shared between the `<img>` and `<canvas>` elements. */
|
||||
const baseClassName = classNames('w-full h-full block', {
|
||||
'object-contain': letterboxed,
|
||||
'object-cover': !letterboxed,
|
||||
});
|
||||
|
||||
return (
|
||||
<div data-testid='still-image-container' className={classNames(className, 'still-image', { 'still-image--play-on-hover': hoverToPlay })} style={style}>
|
||||
<img src={src} alt={alt} ref={img} onLoad={handleImageLoad} />
|
||||
{hoverToPlay && <canvas ref={canvas} />}
|
||||
<div
|
||||
data-testid='still-image-container'
|
||||
className={classNames(className, 'relative group overflow-hidden isolate')}
|
||||
style={style}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
ref={img}
|
||||
onLoad={handleImageLoad}
|
||||
className={classNames(baseClassName, {
|
||||
'invisible group-hover:visible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
|
||||
{hoverToPlay && (
|
||||
<canvas
|
||||
ref={canvas}
|
||||
className={classNames(baseClassName, {
|
||||
'group-hover:invisible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(hoverToPlay && showExt) && (
|
||||
<div className='group-hover:hidden absolute opacity-90 left-2 bottom-2 pointer-events-none'>
|
||||
<ExtensionBadge ext='GIF' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface IExtensionBadge {
|
||||
/** File extension. */
|
||||
ext: string,
|
||||
}
|
||||
|
||||
/** Badge displaying a file extension. */
|
||||
const ExtensionBadge: React.FC<IExtensionBadge> = ({ ext }) => {
|
||||
return (
|
||||
<div className='inline-flex items-center px-2 py-0.5 rounded text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100'>
|
||||
{ext}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -50,7 +50,7 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-left text-sm hover:underline' onClick={handleTranslate}>
|
||||
<button className='text-primary-600 dark:text-accent-blue hover:text-primary-700 dark:hover:text-accent-blue text-start text-sm hover:underline' onClick={handleTranslate}>
|
||||
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
|
||||
|
@ -25,7 +25,7 @@ const Avatar = (props: IAvatar) => {
|
|||
|
||||
return (
|
||||
<StillImage
|
||||
className={classNames('rounded-full overflow-hidden', className)}
|
||||
className={classNames('rounded-full', className)}
|
||||
style={style}
|
||||
src={src}
|
||||
alt='Avatar'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import Icon from '../icon/icon';
|
||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { HStack, Text } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
|
||||
const sizes = {
|
||||
|
@ -62,7 +62,7 @@ const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }):
|
|||
const backAttributes = backHref ? { to: backHref } : { onClick: onBackClick };
|
||||
|
||||
return (
|
||||
<Comp {...backAttributes} className='mr-2 text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}>
|
||||
<Comp {...backAttributes} className='text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}>
|
||||
<SvgIcon src={require('@tabler/icons/arrow-left.svg')} className='h-6 w-6' />
|
||||
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
|
||||
</Comp>
|
||||
|
@ -70,11 +70,11 @@ const CardHeader: React.FC<ICardHeader> = ({ children, backHref, onBackClick }):
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='mb-4 flex flex-row items-center'>
|
||||
<HStack alignItems='center' space={2} className='mb-4'>
|
||||
{renderBackButton()}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -47,8 +47,7 @@ const EmojiSelector: React.FC<IEmojiSelector> = ({ emojis, onReact, visible = fa
|
|||
|
||||
return (
|
||||
<HStack
|
||||
space={2}
|
||||
className={classNames('bg-white dark:bg-gray-900 p-3 rounded-full shadow-md z-[999] w-max')}
|
||||
className={classNames('gap-2 bg-white dark:bg-gray-900 p-3 rounded-full shadow-md z-[999] w-max max-w-[100vw] flex-wrap')}
|
||||
>
|
||||
{Array.from(emojis).map((emoji, i) => (
|
||||
<EmojiButton
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import Emoji from '../emoji';
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
import HStack from '../hstack/hstack';
|
||||
|
||||
/** Container element to house form actions. */
|
||||
const FormActions: React.FC = ({ children }) => (
|
||||
<div className='flex justify-end space-x-2'>
|
||||
<HStack space={2} justifyContent='end'>
|
||||
{children}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
export default FormActions;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface IForm {
|
||||
/** Form submission event handler. */
|
||||
|
|
|
@ -22,8 +22,10 @@ const spaces = {
|
|||
1: 'space-x-1',
|
||||
1.5: 'space-x-1.5',
|
||||
2: 'space-x-2',
|
||||
2.5: 'space-x-2.5',
|
||||
3: 'space-x-3',
|
||||
4: 'space-x-4',
|
||||
5: 'space-x-5',
|
||||
6: 'space-x-6',
|
||||
8: 'space-x-8',
|
||||
};
|
||||
|
@ -59,7 +61,7 @@ const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
|||
<Elem
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex', {
|
||||
className={classNames('flex rtl:space-x-reverse', {
|
||||
// @ts-ignore
|
||||
[alignItemsOptions[alignItems]]: typeof alignItems !== 'undefined',
|
||||
// @ts-ignore
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../../../jest/test-helpers';
|
||||
import SvgIcon from '../svg-icon';
|
||||
|
|
|
@ -43,6 +43,7 @@ export { default as RadioButton } from './radio-button/radio-button';
|
|||
export { default as Select } from './select/select';
|
||||
export { default as Spinner } from './spinner/spinner';
|
||||
export { default as Stack } from './stack/stack';
|
||||
export { default as Streamfield } from './streamfield/streamfield';
|
||||
export { default as Tabs } from './tabs/tabs';
|
||||
export { default as TagInput } from './tag-input/tag-input';
|
||||
export { default as Text } from './text/text';
|
||||
|
|
|
@ -90,16 +90,15 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
['normal', 'search'].includes(theme),
|
||||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
||||
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white': theme === 'search',
|
||||
'pr-7': isPassword || append,
|
||||
'pr-7 rtl:pl-7 rtl:pr-3': isPassword || append,
|
||||
'text-red-600 border-red-600': hasError,
|
||||
'pl-8': typeof icon !== 'undefined',
|
||||
'pl-16': typeof prepend !== 'undefined',
|
||||
}, className)}
|
||||
/>
|
||||
|
||||
{/* eslint-disable-next-line no-nested-ternary */}
|
||||
{append ? (
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3'>
|
||||
<div className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto flex items-center pr-3'>
|
||||
{append}
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -112,7 +111,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
intl.formatMessage(messages.showPassword)
|
||||
}
|
||||
>
|
||||
<div className='absolute inset-y-0 right-0 flex items-center'>
|
||||
<div className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto flex items-center'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={togglePassword}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import Button from '../button/button';
|
||||
import { ButtonThemes } from '../button/useButtonStyles';
|
||||
import IconButton from '../icon-button/icon-button';
|
||||
import Stack from '../stack/stack';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
|
@ -82,7 +83,7 @@ const Modal: React.FC<IModal> = ({
|
|||
}, [skipFocus, buttonRef]);
|
||||
|
||||
return (
|
||||
<div data-testid='modal' className={classNames('block w-full p-6 mx-auto text-left align-middle transition-all transform bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-xl rounded-2xl pointer-events-auto', widths[width])}>
|
||||
<div data-testid='modal' className={classNames('block w-full p-6 mx-auto text-start align-middle transition-all transform bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-xl rounded-2xl pointer-events-auto', widths[width])}>
|
||||
<div className='sm:flex sm:items-start w-full justify-between'>
|
||||
<div className='w-full'>
|
||||
{title && (
|
||||
|
@ -127,7 +128,7 @@ const Modal: React.FC<IModal> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className='flex flex-row space-x-2'>
|
||||
<Stack space={2}>
|
||||
{secondaryAction && (
|
||||
<Button
|
||||
theme='secondary'
|
||||
|
@ -146,7 +147,7 @@ const Modal: React.FC<IModal> = ({
|
|||
>
|
||||
{confirmationText}
|
||||
</Button>
|
||||
</div>
|
||||
</Stack>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import HStack from '../hstack/hstack';
|
||||
|
||||
interface IRadioButton {
|
||||
value: string
|
||||
checked?: boolean
|
||||
|
@ -16,7 +18,7 @@ const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, l
|
|||
const formFieldId: string = useMemo(() => `radio-${uuidv4()}`, []);
|
||||
|
||||
return (
|
||||
<div className='flex items-center'>
|
||||
<HStack alignItems='center' space={3}>
|
||||
<input
|
||||
type='radio'
|
||||
name={name}
|
||||
|
@ -27,10 +29,10 @@ const RadioButton: React.FC<IRadioButton> = ({ name, value, checked, onChange, l
|
|||
className='h-4 w-4 border-gray-300 text-primary-600 focus:ring-primary-500'
|
||||
/>
|
||||
|
||||
<label htmlFor={formFieldId} className='ml-3 block text-sm font-medium text-gray-700'>
|
||||
<label htmlFor={formFieldId} className='block text-sm font-medium text-gray-700'>
|
||||
{label}
|
||||
</label>
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
interface ISelect extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
||||
children: Iterable<React.ReactNode>,
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
useTabsContext,
|
||||
} from '@reach/tabs';
|
||||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import Counter from '../counter/counter';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'onKeyDown' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
|
||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'required' | 'disabled' | 'rows' | 'readOnly' | 'onKeyDown' | 'onPaste'> {
|
||||
/** Put the cursor into the input on mount. */
|
||||
autoFocus?: boolean,
|
||||
/** Allows the textarea height to grow while typing */
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Text, IconButton } from 'soapbox/components/ui';
|
||||
import HStack from 'soapbox/components/ui/hstack/hstack';
|
||||
import Stack from 'soapbox/components/ui/stack/stack';
|
||||
import { HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IWidgetTitle {
|
||||
/** Title text for the widget. */
|
||||
|
|
|
@ -2,7 +2,7 @@ import classNames from 'clsx';
|
|||
import React from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import Icon from 'soapbox/components/ui/icon/icon';
|
||||
import { Icon } from 'soapbox/components/ui';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -49,6 +49,8 @@ import ErrorBoundary from '../components/error-boundary';
|
|||
import UI from '../features/ui';
|
||||
import { store } from '../store';
|
||||
|
||||
const RTL_LOCALES = ['ar', 'ckb', 'fa', 'he'];
|
||||
|
||||
// Configure global functions for developers
|
||||
createGlobals(store);
|
||||
|
||||
|
@ -277,7 +279,7 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
|||
<>
|
||||
<Helmet>
|
||||
<html lang={locale} className={classNames('h-full', { dark: darkMode })} />
|
||||
<body className={bodyClass} />
|
||||
<body className={bodyClass} dir={RTL_LOCALES.includes(locale) ? 'rtl' : undefined} />
|
||||
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
|
||||
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
|
||||
<meta name='theme-color' content={soapboxConfig.brandColor} />
|
||||
|
|
|
@ -74,6 +74,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, displayWidth, onOpenMedia
|
|||
src={attachment.preview_url}
|
||||
alt={attachment.description}
|
||||
style={{ objectPosition: `${x}% ${y}%` }}
|
||||
className='w-full h-full rounded-lg overflow-hidden'
|
||||
/>
|
||||
);
|
||||
} else if (['gifv', 'video'].indexOf(attachment.type) !== -1) {
|
||||
|
|
|
@ -19,7 +19,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import Badge from 'soapbox/components/badge';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { HStack, IconButton, Menu, MenuButton, MenuItem, MenuList, MenuLink, MenuDivider, Avatar } from 'soapbox/components/ui';
|
||||
import { Avatar, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import MovedNote from 'soapbox/features/account-timeline/components/moved-note';
|
||||
import ActionButton from 'soapbox/features/ui/components/action-button';
|
||||
|
@ -71,6 +71,7 @@ const messages = defineMessages({
|
|||
userEndorsed: { id: 'account.endorse.success', defaultMessage: 'You are now featuring @{acct} on your profile' },
|
||||
userUnendorsed: { id: 'account.unendorse.success', defaultMessage: 'You are no longer featuring @{acct}' },
|
||||
profileExternal: { id: 'account.profile_external', defaultMessage: 'View profile on {domain}' },
|
||||
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
|
||||
});
|
||||
|
||||
interface IHeader {
|
||||
|
@ -108,13 +109,13 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
</div>
|
||||
|
||||
<div className='px-4 sm:px-6'>
|
||||
<div className='-mt-12 flex items-end space-x-5'>
|
||||
<HStack alignItems='bottom' space={5} className='-mt-12'>
|
||||
<div className='flex relative'>
|
||||
<div
|
||||
className='h-24 w-24 bg-gray-400 rounded-full ring-4 ring-white dark:ring-gray-800'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -573,13 +574,12 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
)}
|
||||
|
||||
<div>
|
||||
<div className='relative h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50'>
|
||||
<div className='relative flex flex-col justify-center h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50 overflow-hidden isolate'>
|
||||
{account.header && (
|
||||
<a href={account.header} onClick={handleHeaderClick} target='_blank'>
|
||||
<StillImage
|
||||
src={account.header}
|
||||
alt='Profile Header'
|
||||
className='absolute inset-0 object-cover md:rounded-t-xl'
|
||||
alt={intl.formatMessage(messages.header)}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
|
@ -593,19 +593,19 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
</div>
|
||||
|
||||
<div className='px-4 sm:px-6'>
|
||||
<div className='-mt-12 flex items-end space-x-5'>
|
||||
<HStack className='-mt-12' alignItems='bottom' space={5}>
|
||||
<div className='flex'>
|
||||
<a href={account.avatar} onClick={handleAvatarClick} target='_blank'>
|
||||
<Avatar
|
||||
src={account.avatar}
|
||||
size={96}
|
||||
className='h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
|
||||
className='relative h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='mt-6 flex justify-end w-full sm:pb-1'>
|
||||
<div className='mt-10 flex flex-row space-y-0 space-x-2'>
|
||||
<HStack space={2} className='mt-10'>
|
||||
<SubscriptionButton account={account} />
|
||||
|
||||
{ownAccount && (
|
||||
|
@ -629,13 +629,13 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
|
||||
return (
|
||||
<Comp key={idx} {...itemProps} className='group'>
|
||||
<div className='flex items-center'>
|
||||
<HStack space={3} alignItems='center'>
|
||||
{menuItem.icon && (
|
||||
<SvgIcon src={menuItem.icon} className='mr-3 h-5 w-5 text-gray-400 flex-none group-hover:text-gray-500' />
|
||||
<SvgIcon src={menuItem.icon} className='h-5 w-5 text-gray-400 flex-none group-hover:text-gray-500' />
|
||||
)}
|
||||
|
||||
<div className='truncate'>{menuItem.text}</div>
|
||||
</div>
|
||||
</HStack>
|
||||
</Comp>
|
||||
);
|
||||
}
|
||||
|
@ -648,9 +648,9 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
{renderMessageButton()}
|
||||
|
||||
<ActionButton account={account} />
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -2,8 +2,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Stack, HStack, Card, Avatar, Text, Icon } from 'soapbox/components/ui';
|
||||
import IconButton from 'soapbox/components/ui/icon-button/icon-button';
|
||||
import { Avatar, Card, HStack, Icon, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import StatusCard from 'soapbox/features/status/components/card';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { AdKeys } from 'soapbox/queries/ads';
|
||||
|
|
|
@ -2,9 +2,9 @@ import React, { useCallback } from 'react';
|
|||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { addToAliases } from 'soapbox/actions/aliases';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import AccountComponent from 'soapbox/components/account';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
@ -47,23 +47,17 @@ const Account: React.FC<IAccount> = ({ accountId, aliases }) => {
|
|||
|
||||
if (!added && accountId !== me) {
|
||||
button = (
|
||||
<div className='account__relationship'>
|
||||
<IconButton src={require('@tabler/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={handleOnAdd} />
|
||||
</div>
|
||||
<IconButton src={require('@tabler/icons/plus.svg')} iconClassName='h-5 w-5' title={intl.formatMessage(messages.add)} onClick={handleOnAdd} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
{button}
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<AccountComponent account={account} withRelationship={false} />
|
||||
</div>
|
||||
</div>
|
||||
{button}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ const Search: React.FC = () => {
|
|||
placeholder={intl.formatMessage(messages.search)}
|
||||
/>
|
||||
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
|
||||
<div role='button' tabIndex={hasValue ? 0 : -1} className='search__icon' onClick={handleClear}>
|
||||
<Icon src={require('@tabler/icons/backspace.svg')} aria-label={intl.formatMessage(messages.search)} className={classNames('svg-icon--backspace', { active: hasValue })} />
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
@ -26,32 +26,35 @@ const LoginPage = () => {
|
|||
const [mfaToken, setMfaToken] = useState(token || '');
|
||||
const [shouldRedirect, setShouldRedirect] = useState(false);
|
||||
|
||||
const getFormData = (form: HTMLFormElement) => {
|
||||
return Object.fromEntries(
|
||||
const getFormData = (form: HTMLFormElement) =>
|
||||
Object.fromEntries(
|
||||
Array.from(form).map((i: any) => [i.name, i.value]),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
const { username, password } = getFormData(event.target as HTMLFormElement);
|
||||
dispatch(logIn(username, password)).then(({ access_token }) => {
|
||||
return dispatch(verifyCredentials(access_token as string))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance() as any));
|
||||
}).then((account: { id: string }) => {
|
||||
dispatch(closeModal());
|
||||
setShouldRedirect(true);
|
||||
if (typeof me === 'string') {
|
||||
dispatch(switchAccount(account.id));
|
||||
}
|
||||
}).catch((error: AxiosError) => {
|
||||
const data: any = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaAuthNeeded(true);
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
dispatch(logIn(username, password))
|
||||
.then(({ access_token }) => dispatch(verifyCredentials(access_token as string)))
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(async (account) => {
|
||||
await dispatch(fetchInstance());
|
||||
return account;
|
||||
})
|
||||
.then((account: { id: string }) => {
|
||||
dispatch(closeModal());
|
||||
if (typeof me === 'string') {
|
||||
dispatch(switchAccount(account.id));
|
||||
} else {
|
||||
setShouldRedirect(true);
|
||||
}
|
||||
}).catch((error: AxiosError) => {
|
||||
const data: any = error.response?.data;
|
||||
if (data?.error === 'mfa_required') {
|
||||
setMfaAuthNeeded(true);
|
||||
setMfaToken(data.mfa_token);
|
||||
}
|
||||
setIsLoading(false);
|
||||
});
|
||||
setIsLoading(true);
|
||||
event.preventDefault();
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
|
@ -11,6 +11,7 @@ const token = new URLSearchParams(window.location.search).get('reset_password_to
|
|||
|
||||
const messages = defineMessages({
|
||||
resetPasswordFail: { id: 'reset_password.fail', defaultMessage: 'Expired token, please try again.' },
|
||||
passwordPlaceholder: { id: 'reset_password.password.placeholder', defaultMessage: 'Placeholder' },
|
||||
});
|
||||
|
||||
const Statuses = {
|
||||
|
@ -66,11 +67,11 @@ const PasswordResetConfirm = () => {
|
|||
|
||||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<FormGroup labelText='Password' errors={renderErrors()}>
|
||||
<FormGroup labelText={<FormattedMessage id='reset_password.password.label' defaultMessage='Password' />} errors={renderErrors()}>
|
||||
<Input
|
||||
type='password'
|
||||
name='password'
|
||||
placeholder='Password'
|
||||
placeholder={intl.formatMessage(messages.passwordPlaceholder)}
|
||||
onChange={onChange}
|
||||
required
|
||||
/>
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
||||
import AutosuggestTextarea from 'soapbox/components/autosuggest-textarea';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Button, Stack } from 'soapbox/components/ui';
|
||||
import { Button, HStack, Stack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useCompose, useFeatures, usePrevious } from 'soapbox/hooks';
|
||||
import { isMobile } from 'soapbox/is-mobile';
|
||||
|
||||
|
@ -221,7 +221,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
}, [focusDate]);
|
||||
|
||||
const renderButtons = useCallback(() => (
|
||||
<div className='flex items-center space-x-2'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
|
@ -229,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{features.spoilers && <SpoilerButton composeId={id} />}
|
||||
{features.richText && <MarkdownButton composeId={id} />}
|
||||
</div>
|
||||
</HStack>
|
||||
), [features, id]);
|
||||
|
||||
const condensed = shouldCondense && !composeFocused && isEmpty() && !isUploading;
|
||||
|
@ -335,16 +335,18 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
>
|
||||
{renderButtons()}
|
||||
|
||||
<div className='flex items-center space-x-4 ml-auto'>
|
||||
<HStack space={4} alignItems='center' className='ml-auto rtl:ml-0 rtl:mr-auto'>
|
||||
{maxTootChars && (
|
||||
<div className='flex items-center space-x-1'>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<TextCharacterCounter max={maxTootChars} text={text} />
|
||||
<VisualCharacterCounter max={maxTootChars} text={text} />
|
||||
</div>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
<Button type='submit' theme='primary' text={publishText} disabled={disabledButton} />
|
||||
</div>
|
||||
</HStack>
|
||||
{/* <HStack alignItems='center' space={4}>
|
||||
</HStack> */}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
|||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { buildCustomEmojis } from '../../../emoji/emoji';
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from '../../../emoji/emoji';
|
||||
|
||||
import { EmojiPicker } from './emoji-picker-dropdown';
|
||||
import ModifierPicker from './modifier-picker';
|
||||
|
@ -14,19 +14,6 @@ import type { Emoji } from 'soapbox/components/autosuggest-emoji';
|
|||
const backgroundImageFn = () => require('emoji-datasource/img/twitter/sheets/32.png');
|
||||
const listenerOptions = supportsPassiveEvents ? { passive: true } : false;
|
||||
|
||||
const categoriesSort = [
|
||||
'recent',
|
||||
'custom',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
];
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search…' },
|
||||
|
@ -71,6 +58,20 @@ const EmojiPickerMenu: React.FC<IEmojiPickerMenu> = ({
|
|||
|
||||
const [modifierOpen, setModifierOpen] = useState(false);
|
||||
|
||||
const categoriesSort = [
|
||||
'recent',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
];
|
||||
|
||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(customEmojis) as Set<string>).sort());
|
||||
|
||||
const handleDocumentClick = useCallback(e => {
|
||||
if (node.current && !node.current.contains(e.target)) {
|
||||
onClose();
|
||||
|
|
|
@ -169,7 +169,7 @@ const PollForm: React.FC<IPollForm> = ({ composeId }) => {
|
|||
|
||||
<Divider />
|
||||
|
||||
<button type='button' onClick={handleToggleMultiple} className='text-left'>
|
||||
<button type='button' onClick={handleToggleMultiple} className='text-start'>
|
||||
<HStack alignItems='center' justifyContent='between'>
|
||||
<Stack>
|
||||
<Text weight='medium'>
|
||||
|
|
|
@ -149,7 +149,7 @@ const Search = (props: ISearch) => {
|
|||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
||||
className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto px-3 flex items-center cursor-pointer'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<SvgIcon
|
||||
|
|
|
@ -33,7 +33,7 @@ const CryptoAddress: React.FC<ICryptoAddress> = (props): JSX.Element => {
|
|||
<Stack>
|
||||
<HStack alignItems='center' className='mb-1'>
|
||||
<CryptoIcon
|
||||
className='flex items-start justify-center w-6 mr-2.5'
|
||||
className='flex items-start justify-center w-6 mr-2.5 rtl:ml-2.5 rtl:mr-0'
|
||||
ticker={ticker}
|
||||
title={title}
|
||||
/>
|
||||
|
@ -41,12 +41,12 @@ const CryptoAddress: React.FC<ICryptoAddress> = (props): JSX.Element => {
|
|||
<Text weight='bold'>{title || ticker.toUpperCase()}</Text>
|
||||
|
||||
<HStack alignItems='center' className='ml-auto'>
|
||||
<a className='text-gray-500 ml-1' href='#' onClick={handleModalClick}>
|
||||
<a className='text-gray-500 ml-1 rtl:ml-0 rtl:mr-1' href='#' onClick={handleModalClick}>
|
||||
<Icon src={require('@tabler/icons/qrcode.svg')} size={20} />
|
||||
</a>
|
||||
|
||||
{explorerUrl && (
|
||||
<a className='text-gray-500 ml-1' href={explorerUrl} target='_blank'>
|
||||
<a className='text-gray-500 ml-1 rtl:ml-0 rtl:mr-1' href={explorerUrl} target='_blank'>
|
||||
<Icon src={require('@tabler/icons/external-link.svg')} size={20} />
|
||||
</a>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { deleteAccount } from 'soapbox/actions/security';
|
||||
|
|
|
@ -29,6 +29,7 @@ const isJSONValid = (text: any): boolean => {
|
|||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.settings_store', defaultMessage: 'Settings store' },
|
||||
advanced: { id: 'developers.settings_store.advanced', defaultMessage: 'Advanced settings' },
|
||||
hint: { id: 'developers.settings_store.hint', defaultMessage: 'It is possible to directly edit your user settings here. BE CAREFUL! Editing this section can break your account, and you will only be able to recover through the API.' },
|
||||
});
|
||||
|
||||
|
@ -98,7 +99,7 @@ const SettingsStore: React.FC = () => {
|
|||
</Form>
|
||||
|
||||
<CardHeader>
|
||||
<CardTitle title='Advanced settings' />
|
||||
<CardTitle title={intl.formatMessage(messages.advanced)} />
|
||||
</CardHeader>
|
||||
|
||||
<List>
|
||||
|
|
|
@ -5,14 +5,13 @@ import { directComposeById } from 'soapbox/actions/compose';
|
|||
import { connectDirectStream } from 'soapbox/actions/streaming';
|
||||
import { expandDirectTimeline } from 'soapbox/actions/timelines';
|
||||
import AccountSearch from 'soapbox/components/account-search';
|
||||
import ColumnHeader from 'soapbox/components/column-header';
|
||||
import { Column } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||
heading: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||
searchPlaceholder: { id: 'direct.search_placeholder', defaultMessage: 'Send a message to…' },
|
||||
});
|
||||
|
||||
|
@ -20,8 +19,6 @@ const DirectTimeline = () => {
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const hasUnread = useAppSelector((state) => (state.timelines.get('direct')?.unread || 0) > 0);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(expandDirectTimeline());
|
||||
const disconnect = dispatch(connectDirectStream());
|
||||
|
@ -40,13 +37,7 @@ const DirectTimeline = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
|
||||
<ColumnHeader
|
||||
icon='envelope'
|
||||
active={hasUnread}
|
||||
title={intl.formatMessage(messages.title)}
|
||||
/>
|
||||
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<AccountSearch
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
onSelected={handleSuggestion}
|
||||
|
@ -57,7 +48,7 @@ const DirectTimeline = () => {
|
|||
timelineId='direct'
|
||||
onLoadMore={handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||
divideType='space'
|
||||
divideType='border'
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changeEmail } from 'soapbox/actions/security';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { changePassword } from 'soapbox/actions/security';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
|
@ -17,25 +17,28 @@ const ProfilePreview: React.FC<IProfilePreview> = ({ account }) => {
|
|||
|
||||
return (
|
||||
<div className='bg-white dark:bg-gray-800 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden'>
|
||||
<div>
|
||||
<div className='relative w-full h-32 md:rounded-t-lg bg-gray-200 dark:bg-gray-900/50'>
|
||||
<StillImage alt='' src={account.header} className='absolute inset-0 object-cover md:rounded-t-lg' />
|
||||
</div>
|
||||
<div className='relative overflow-hidden isolate w-full h-32 md:rounded-t-lg bg-gray-200 dark:bg-gray-900/50'>
|
||||
<StillImage src={account.header} />
|
||||
</div>
|
||||
|
||||
<HStack space={3} alignItems='center' className='p-3'>
|
||||
<div className='relative'>
|
||||
<div className='h-12 w-12 bg-gray-400 rounded-full'>
|
||||
<StillImage alt='' className='h-12 w-12 rounded-full' src={account.avatar} />
|
||||
</div>
|
||||
<Avatar className='bg-gray-400' src={account.avatar} />
|
||||
|
||||
{account.verified && <div className='absolute -top-1.5 -right-1.5'><VerificationBadge /></div>}
|
||||
{account.verified && (
|
||||
<div className='absolute -top-1.5 -right-1.5'>
|
||||
<VerificationBadge />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Stack className='truncate'>
|
||||
<Text weight='medium' size='sm' truncate>
|
||||
{account.display_name}
|
||||
</Text>
|
||||
<Text
|
||||
weight='medium'
|
||||
size='sm'
|
||||
truncate
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
/>
|
||||
<Text theme='muted' size='sm'>@{displayFqn ? account.fqn : account.acct}</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
|
|
|
@ -15,16 +15,17 @@ import {
|
|||
FormGroup,
|
||||
HStack,
|
||||
Input,
|
||||
Streamfield,
|
||||
Textarea,
|
||||
Toggle,
|
||||
} from 'soapbox/components/ui';
|
||||
import Streamfield, { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import { useAppSelector, useAppDispatch, useOwnAccount, useFeatures } from 'soapbox/hooks';
|
||||
import { normalizeAccount } from 'soapbox/normalizers';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import ProfilePreview from './components/profile-preview';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import type { Account } from 'soapbox/types/entities';
|
||||
|
||||
/**
|
||||
|
@ -198,7 +199,10 @@ const EditProfile: React.FC = () => {
|
|||
const handleSubmit: React.FormEventHandler = (event) => {
|
||||
const promises = [];
|
||||
|
||||
promises.push(dispatch(patchMe(data, true)));
|
||||
const params = { ...data };
|
||||
if (params.fields_attributes?.length === 0) params.fields_attributes = [{ name: '', value: '' }];
|
||||
|
||||
promises.push(dispatch(patchMe(params, true)));
|
||||
|
||||
if (features.muteStrangers) {
|
||||
promises.push(
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
|
|
|
@ -88,5 +88,10 @@ describe('emoji', () => {
|
|||
expect(emojify('💂♀️💂♂️'))
|
||||
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀️" title=":female-guard:" src="/packs/emoji/1f482-200d-2640-fe0f.svg"><img draggable="false" class="emojione" alt="💂\u200D♂️" title=":male-guard:" src="/packs/emoji/1f482-200d-2642-fe0f.svg">');
|
||||
});
|
||||
|
||||
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
|
||||
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
|
||||
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/packs/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Binary file not shown.
|
@ -4,7 +4,7 @@ import React from 'react';
|
|||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { Map as ImmutableMap } from 'immutable';
|
||||
|
@ -16,6 +16,23 @@ const hasRestrictions = (remoteInstance: ImmutableMap<string, any>): boolean =>
|
|||
.reduce((acc: boolean, value: boolean) => acc || value, false);
|
||||
};
|
||||
|
||||
interface IRestriction {
|
||||
icon: string,
|
||||
children: React.ReactNode,
|
||||
}
|
||||
|
||||
const Restriction: React.FC<IRestriction> = ({ icon, children }) => {
|
||||
return (
|
||||
<HStack space={3}>
|
||||
<Icon className='flex-none w-5 h-5' src={icon} />
|
||||
|
||||
<Text theme='muted'>
|
||||
{children}
|
||||
</Text>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
interface IInstanceRestrictions {
|
||||
remoteInstance: ImmutableMap<string, any>,
|
||||
}
|
||||
|
@ -40,57 +57,52 @@ const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance
|
|||
|
||||
if (followers_only) {
|
||||
items.push((
|
||||
<Text key='followers_only' className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/lock.svg')} />
|
||||
<Restriction key='followersOnly' icon={require('@tabler/icons/lock.svg')}>
|
||||
<FormattedMessage
|
||||
id='federation_restriction.followers_only'
|
||||
defaultMessage='Hidden except to followers'
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
));
|
||||
} else if (federated_timeline_removal) {
|
||||
items.push((
|
||||
<Text key='federated_timeline_removal' className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/lock-open.svg')} />
|
||||
<Restriction key='federatedTimelineRemoval' icon={require('@tabler/icons/lock-open.svg')}>
|
||||
<FormattedMessage
|
||||
id='federation_restriction.federated_timeline_removal'
|
||||
defaultMessage='Fediverse timeline removal'
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
));
|
||||
}
|
||||
|
||||
if (fullMediaRemoval) {
|
||||
items.push((
|
||||
<Text key='full_media_removal' className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/photo-off.svg')} />
|
||||
<Restriction key='fullMediaRemoval' icon={require('@tabler/icons/photo-off.svg')}>
|
||||
<FormattedMessage
|
||||
id='federation_restriction.full_media_removal'
|
||||
defaultMessage='Full media removal'
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
));
|
||||
} else if (partialMediaRemoval) {
|
||||
items.push((
|
||||
<Text key='partial_media_removal' className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/photo-off.svg')} />
|
||||
<Restriction key='partialMediaRemoval' icon={require('@tabler/icons/photo-off.svg')}>
|
||||
<FormattedMessage
|
||||
id='federation_restriction.partial_media_removal'
|
||||
defaultMessage='Partial media removal'
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
));
|
||||
}
|
||||
|
||||
if (!fullMediaRemoval && media_nsfw) {
|
||||
items.push((
|
||||
<Text key='media_nsfw' className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/eye-off.svg')} />
|
||||
<Restriction key='mediaNsfw' icon={require('@tabler/icons/eye-off.svg')}>
|
||||
<FormattedMessage
|
||||
id='federation_restriction.media_nsfw'
|
||||
defaultMessage='Attachments marked NSFW'
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -105,46 +117,45 @@ const InstanceRestrictions: React.FC<IInstanceRestrictions> = ({ remoteInstance
|
|||
|
||||
if (remoteInstance.getIn(['federation', 'reject']) === true) {
|
||||
return (
|
||||
<Text className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/x.svg')} />
|
||||
<Restriction icon={require('@tabler/icons/shield-x.svg')}>
|
||||
<FormattedMessage
|
||||
id='remote_instance.federation_panel.restricted_message'
|
||||
defaultMessage='{siteTitle} blocks all activities from {host}.'
|
||||
values={{ host, siteTitle }}
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
);
|
||||
} else if (hasRestrictions(remoteInstance)) {
|
||||
return [
|
||||
(
|
||||
<Text theme='muted'>
|
||||
return (
|
||||
<>
|
||||
<Restriction icon={require('@tabler/icons/shield-lock.svg')}>
|
||||
<FormattedMessage
|
||||
id='remote_instance.federation_panel.some_restrictions_message'
|
||||
defaultMessage='{siteTitle} has placed some restrictions on {host}.'
|
||||
values={{ host, siteTitle }}
|
||||
/>
|
||||
</Text>
|
||||
),
|
||||
renderRestrictions(),
|
||||
];
|
||||
</Restriction>
|
||||
|
||||
{renderRestrictions()}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Text className='flex items-center gap-2' theme='muted'>
|
||||
<Icon src={require('@tabler/icons/check.svg')} />
|
||||
<Restriction icon={require('@tabler/icons/shield-check.svg')}>
|
||||
<FormattedMessage
|
||||
id='remote_instance.federation_panel.no_restrictions_message'
|
||||
defaultMessage='{siteTitle} has placed no restrictions on {host}.'
|
||||
values={{ host, siteTitle }}
|
||||
/>
|
||||
</Text>
|
||||
</Restriction>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='py-1 pl-4 mb-4 border-solid border-l-[3px] border-gray-300 dark:border-gray-500'>
|
||||
<Stack space={3}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ const SuggestionItem = ({ accountId }: { accountId: string }) => {
|
|||
<HStack alignItems='center' justifyContent='center' space={1}>
|
||||
<Text
|
||||
weight='semibold'
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name }}
|
||||
dangerouslySetInnerHTML={{ __html: account.display_name_html }}
|
||||
truncate
|
||||
align='center'
|
||||
size='sm'
|
||||
|
@ -78,7 +78,7 @@ const FeedSuggestions = () => {
|
|||
</HStack>
|
||||
|
||||
<CardBody>
|
||||
<HStack alignItems='center' className='overflow-x-auto lg:overflow-x-hidden space-x-4 md:space-x-0'>
|
||||
<HStack space={4} alignItems='center' className='overflow-x-auto lg:overflow-x-hidden md:space-x-0'>
|
||||
{suggestedProfiles.slice(0, 4).map((suggestedProfile) => (
|
||||
<SuggestionItem key={suggestedProfile.account} accountId={suggestedProfile.account} />
|
||||
))}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import LandingPage from '..';
|
||||
import { rememberInstance } from '../../../actions/instance';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { prepareRequest } from 'soapbox/actions/consumer-auth';
|
||||
|
@ -106,7 +106,7 @@ const LandingPage = () => {
|
|||
<main className='mt-16 sm:mt-24' data-testid='homepage'>
|
||||
<div className='mx-auto max-w-7xl'>
|
||||
<div className='grid grid-cols-1 lg:grid-cols-12 gap-8 py-12'>
|
||||
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left lg:flex'>
|
||||
<div className='px-4 sm:px-6 sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-start lg:flex'>
|
||||
<div className='w-full'>
|
||||
<Stack space={3}>
|
||||
<h1 className='text-5xl font-extrabold text-transparent text-ellipsis overflow-hidden bg-clip-text bg-gradient-to-br from-accent-500 via-primary-500 to-gradient-end sm:mt-5 sm:leading-none lg:mt-6 lg:text-6xl xl:text-7xl'>
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import { Avatar } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
interface IAccount {
|
||||
accountId: string,
|
||||
}
|
||||
|
||||
const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar src={account.avatar} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Account;
|
|
@ -4,11 +4,11 @@ import { createSelector } from 'reselect';
|
|||
|
||||
import { setupListAdder, resetListAdder } from 'soapbox/actions/lists';
|
||||
import { CardHeader, CardTitle, Modal } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import NewListForm from '../lists/components/new-list-form';
|
||||
|
||||
import Account from './components/account';
|
||||
import List from './components/list';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
@ -58,7 +58,7 @@ const ListAdder: React.FC<IListAdder> = ({ accountId, onClose }) => {
|
|||
title={<FormattedMessage id='list_adder.header_title' defaultMessage='Add or Remove from Lists' />}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
<Account accountId={accountId} />
|
||||
<AccountContainer id={accountId} withRelationship={false} />
|
||||
|
||||
<br />
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { removeFromListEditor, addToListEditor } from 'soapbox/actions/lists';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { Avatar } from 'soapbox/components/ui';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
const messages = defineMessages({
|
||||
remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' },
|
||||
|
@ -20,37 +19,27 @@ interface IAccount {
|
|||
const Account: React.FC<IAccount> = ({ accountId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const getAccount = useCallback(makeGetAccount(), []);
|
||||
|
||||
const account = useAppSelector((state) => getAccount(state, accountId));
|
||||
const isAdded = useAppSelector((state) => state.listEditor.accounts.items.includes(accountId));
|
||||
|
||||
const onRemove = () => dispatch(removeFromListEditor(accountId));
|
||||
const onAdd = () => dispatch(addToListEditor(accountId));
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
let button;
|
||||
|
||||
if (isAdded) {
|
||||
button = <IconButton src={require('@tabler/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
button = <IconButton src={require('@tabler/icons/x.svg')} iconClassName='h-5 w-5' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton src={require('@tabler/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
button = <IconButton src={require('@tabler/icons/plus.svg')} iconClassName='h-5 w-5' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar src={account.avatar} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{button}
|
||||
</div>
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<AccountContainer id={accountId} withRelationship={false} />
|
||||
</div>
|
||||
</div>
|
||||
{button}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { updateNotifications } from 'soapbox/actions/notifications';
|
||||
import { render, screen, rootState, createTestStore } from 'soapbox/jest/test-helpers';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ReactSwipeableViews from 'react-swipeable-views';
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
|
@ -11,6 +11,10 @@ import resizeImage from 'soapbox/utils/resize-image';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
/** Default avatar filenames from various backends */
|
||||
const DEFAULT_AVATARS = [
|
||||
'/avatars/original/missing.png', // Mastodon
|
||||
|
@ -64,7 +68,7 @@ const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
|||
if (error.response?.status === 422) {
|
||||
dispatch(snackbar.error((error.response.data as any).error.replace('Validation failed: ', '')));
|
||||
} else {
|
||||
dispatch(snackbar.error('An unexpected error occurred. Please try again or skip this step.'));
|
||||
dispatch(snackbar.error(messages.error));
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
|
@ -9,7 +9,13 @@ import { useOwnAccount } from 'soapbox/hooks';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
bioPlaceholder: { id: 'onboarding.bio.placeholder', defaultMessage: 'Tell the world a little about yourself…' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const BioStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const account = useOwnAccount();
|
||||
|
@ -32,7 +38,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
|
|||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
dispatch(snackbar.error('An unexpected error occurred. Please try again or skip this step.'));
|
||||
dispatch(snackbar.error(messages.error));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -56,13 +62,13 @@ const BioStep = ({ onNext }: { onNext: () => void }) => {
|
|||
<Stack space={5}>
|
||||
<div className='sm:pt-10 sm:w-2/3 mx-auto'>
|
||||
<FormGroup
|
||||
hintText='Max 500 characters'
|
||||
labelText='Bio'
|
||||
hintText={<FormattedMessage id='onboarding.bio.hint' defaultMessage='Max 500 characters' />}
|
||||
labelText={<FormattedMessage id='edit_profile.fields.bio_label' defaultMessage='Bio' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Textarea
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder='Tell the world a little about yourself…'
|
||||
placeholder={intl.formatMessage(messages.bioPlaceholder)}
|
||||
value={value}
|
||||
maxLength={500}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Button, Card, CardBody, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
|
@ -12,6 +12,11 @@ import resizeImage from 'soapbox/utils/resize-image';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
header: { id: 'account.header.alt', defaultMessage: 'Profile header' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
'/headers/original/missing.png', // Mastodon
|
||||
|
@ -24,6 +29,7 @@ const isDefaultHeader = (url: string) => {
|
|||
};
|
||||
|
||||
const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const account = useOwnAccount();
|
||||
|
||||
|
@ -65,7 +71,7 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
|||
if (error.response?.status === 422) {
|
||||
dispatch(snackbar.error((error.response.data as any).error.replace('Validation failed: ', '')));
|
||||
} else {
|
||||
dispatch(snackbar.error('An unexpected error occurred. Please try again or skip this step.'));
|
||||
dispatch(snackbar.error(messages.error));
|
||||
}
|
||||
});
|
||||
}).catch(console.error);
|
||||
|
@ -90,7 +96,6 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
|||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
||||
<Stack space={10}>
|
||||
<div className='border border-solid border-gray-200 dark:border-gray-800 rounded-lg'>
|
||||
{/* eslint-disable-next-line jsx-a11y/interactive-supports-focus */}
|
||||
<div
|
||||
role='button'
|
||||
className='relative h-24 bg-gray-200 dark:bg-gray-800 rounded-t-md flex items-center justify-center'
|
||||
|
@ -98,7 +103,7 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
|||
{selectedFile || account?.header && (
|
||||
<StillImage
|
||||
src={selectedFile || account.header}
|
||||
alt='Profile Header'
|
||||
alt={intl.formatMessage(messages.header)}
|
||||
className='absolute inset-0 object-cover rounded-t-md'
|
||||
/>
|
||||
)}
|
||||
|
@ -138,7 +143,11 @@ const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
|||
|
||||
<Stack justifyContent='center' space={2}>
|
||||
<Button block theme='primary' type='button' onClick={onNext} disabled={isDefault && isDisabled || isSubmitting}>
|
||||
{isSubmitting ? 'Saving…' : 'Next'}
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{isDisabled && (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { patchMe } from 'soapbox/actions/me';
|
||||
|
@ -9,7 +9,13 @@ import { useOwnAccount } from 'soapbox/hooks';
|
|||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
usernamePlaceholder: { id: 'onboarding.display_name.placeholder', defaultMessage: 'Eg. John Smith' },
|
||||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const account = useOwnAccount();
|
||||
|
@ -43,7 +49,7 @@ const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
|||
if (error.response?.status === 422) {
|
||||
setErrors([(error.response.data as any).error.replace('Validation failed: ', '')]);
|
||||
} else {
|
||||
dispatch(snackbar.error('An unexpected error occurred. Please try again or skip this step.'));
|
||||
dispatch(snackbar.error(messages.error));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -68,12 +74,12 @@ const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
|||
<Stack space={5}>
|
||||
<FormGroup
|
||||
hintText={hintText}
|
||||
labelText='Display name'
|
||||
labelText={<FormattedMessage id='onboarding.display_name.label' defaultMessage='Display name' />}
|
||||
errors={errors}
|
||||
>
|
||||
<Input
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
placeholder='Eg. John Smith'
|
||||
placeholder={intl.formatMessage(messages.usernamePlaceholder)}
|
||||
type='text'
|
||||
value={value}
|
||||
maxLength={30}
|
||||
|
@ -88,7 +94,11 @@ const DisplayNameStep = ({ onNext }: { onNext: () => void }) => {
|
|||
disabled={isDisabled || isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isSubmitting ? 'Saving…' : 'Next'}
|
||||
{isSubmitting ? (
|
||||
<FormattedMessage id='onboarding.saving' defaultMessage='Saving…' />
|
||||
) : (
|
||||
<FormattedMessage id='onboarding.next' defaultMessage='Next' />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button block theme='tertiary' type='button' onClick={onNext}>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Account from 'soapbox/components/account';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import debounce from 'lodash/debounce';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { randomIntFromInterval, generateText } from '../utils';
|
||||
|
||||
|
|
|
@ -5,17 +5,18 @@ import { randomIntFromInterval, generateText } from '../utils';
|
|||
interface IPlaceholderDisplayName {
|
||||
maxLength: number,
|
||||
minLength: number,
|
||||
withSuffix?: boolean,
|
||||
}
|
||||
|
||||
/** Fake display name to show when data is loading. */
|
||||
const PlaceholderDisplayName: React.FC<IPlaceholderDisplayName> = ({ minLength, maxLength }) => {
|
||||
const PlaceholderDisplayName: React.FC<IPlaceholderDisplayName> = ({ minLength, maxLength, withSuffix = true }) => {
|
||||
const length = randomIntFromInterval(maxLength, minLength);
|
||||
const acctLength = randomIntFromInterval(maxLength, minLength);
|
||||
|
||||
return (
|
||||
<div className='flex flex-col text-primary-50 dark:text-primary-800'>
|
||||
<p>{generateText(length)}</p>
|
||||
<p>{generateText(acctLength)}</p>
|
||||
{withSuffix && <p>{generateText(acctLength)}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
|
||||
import PlaceholderAvatar from './placeholder-avatar';
|
||||
import PlaceholderDisplayName from './placeholder-display-name';
|
||||
|
@ -13,7 +15,7 @@ const PlaceholderNotification = () => (
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<div className='flex space-x-3 items-center'>
|
||||
<HStack space={3} alignItems='center'>
|
||||
<div className='flex-shrink-0'>
|
||||
<PlaceholderAvatar size={48} />
|
||||
</div>
|
||||
|
@ -21,7 +23,7 @@ const PlaceholderNotification = () => (
|
|||
<div className='min-w-0 flex-1'>
|
||||
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
||||
</div>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<div className='mt-4'>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
|
||||
import PlaceholderAvatar from './placeholder-avatar';
|
||||
import PlaceholderDisplayName from './placeholder-display-name';
|
||||
|
@ -19,7 +21,7 @@ const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ thread = false }) =>
|
|||
>
|
||||
<div className='w-full animate-pulse overflow-hidden'>
|
||||
<div>
|
||||
<div className='flex space-x-3 items-center'>
|
||||
<HStack space={3} alignItems='center'>
|
||||
<div className='flex-shrink-0'>
|
||||
<PlaceholderAvatar size={42} />
|
||||
</div>
|
||||
|
@ -27,7 +29,7 @@ const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ thread = false }) =>
|
|||
<div className='min-w-0 flex-1'>
|
||||
<PlaceholderDisplayName minLength={3} maxLength={25} />
|
||||
</div>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<div className='mt-4 status__content-wrapper'>
|
||||
|
|
|
@ -15,6 +15,7 @@ import Sonar from './sonar';
|
|||
import type { AxiosError } from 'axios';
|
||||
|
||||
const messages = defineMessages({
|
||||
menu: { id: 'header.menu.title', defaultMessage: 'Open menu' },
|
||||
home: { id: 'header.home.label', defaultMessage: 'Home' },
|
||||
login: { id: 'header.login.label', defaultMessage: 'Log in' },
|
||||
register: { id: 'header.register.label', defaultMessage: 'Register' },
|
||||
|
@ -50,14 +51,12 @@ const Header = () => {
|
|||
setLoading(true);
|
||||
|
||||
dispatch(logIn(username, password) as any)
|
||||
.then(({ access_token }: { access_token: string }) => {
|
||||
return (
|
||||
dispatch(verifyCredentials(access_token) as any)
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()))
|
||||
.then(() => setShouldRedirect(true))
|
||||
);
|
||||
})
|
||||
.then(({ access_token }: { access_token: string }) => (
|
||||
dispatch(verifyCredentials(access_token) as any)
|
||||
// Refetch the instance for authenticated fetch
|
||||
.then(() => dispatch(fetchInstance()))
|
||||
.then(() => setShouldRedirect(true))
|
||||
))
|
||||
.catch((error: AxiosError) => {
|
||||
setLoading(false);
|
||||
|
||||
|
@ -81,7 +80,7 @@ const Header = () => {
|
|||
</div>
|
||||
|
||||
<IconButton
|
||||
title='Open Menu'
|
||||
title={intl.formatMessage(messages.menu)}
|
||||
src={require('@tabler/icons/menu-2.svg')}
|
||||
onClick={open}
|
||||
className='md:hidden mr-4 bg-transparent text-gray-700 dark:text-gray-600 hover:text-gray-600'
|
||||
|
@ -94,7 +93,7 @@ const Header = () => {
|
|||
|
||||
</div>
|
||||
|
||||
<div className='ml-10 flex space-x-6 items-center relative z-10'>
|
||||
<HStack space={6} alignItems='center' className='ml-10 relative z-10'>
|
||||
<HStack alignItems='center'>
|
||||
<HStack space={6} alignItems='center' className='hidden md:flex md:mr-6'>
|
||||
{links.get('help') && (
|
||||
|
@ -124,7 +123,7 @@ const Header = () => {
|
|||
</HStack>
|
||||
</HStack>
|
||||
|
||||
<Form className='hidden xl:flex space-x-2 items-center' onSubmit={handleSubmit}>
|
||||
<Form className='hidden xl:flex space-x-2 rtl:space-x-reverse items-center' onSubmit={handleSubmit}>
|
||||
<Input
|
||||
required
|
||||
value={username}
|
||||
|
@ -167,7 +166,7 @@ const Header = () => {
|
|||
{intl.formatMessage(messages.login)}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
|
|
@ -5,8 +5,8 @@ import { useHistory } from 'react-router-dom';
|
|||
import { connectRemoteStream } from 'soapbox/actions/streaming';
|
||||
import { expandRemoteTimeline } from 'soapbox/actions/timelines';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { HStack, Text } from 'soapbox/components/ui';
|
||||
import Column from 'soapbox/features/ui/components/column';
|
||||
import SubNavigation from 'soapbox/components/sub-navigation';
|
||||
import { Column, HStack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useSettings } from 'soapbox/hooks';
|
||||
|
||||
import Timeline from '../ui/components/timeline';
|
||||
|
@ -14,7 +14,7 @@ import Timeline from '../ui/components/timeline';
|
|||
import PinnedHostsPicker from './components/pinned-hosts-picker';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.remote', defaultMessage: 'Federated timeline' },
|
||||
heading: { id: 'column.remote', defaultMessage: 'Federated timeline' },
|
||||
});
|
||||
|
||||
interface IRemoteTimeline {
|
||||
|
@ -65,18 +65,26 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
|
|||
}, [onlyMedia]);
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.title)} heading={instance} transparent withHeader={false}>
|
||||
{instance && <PinnedHostsPicker host={instance} />}
|
||||
{!pinned && <HStack className='mb-4 px-2' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleCloseClick} />
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='remote_timeline.filter_message'
|
||||
defaultMessage='You are viewing the timeline of {instance}.'
|
||||
values={{ instance }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>}
|
||||
<Column label={intl.formatMessage(messages.heading)} transparent withHeader={false}>
|
||||
<div className='px-4 pt-4 sm:p-0'>
|
||||
<SubNavigation message={instance} />
|
||||
|
||||
{instance && <PinnedHostsPicker host={instance} />}
|
||||
|
||||
{!pinned && (
|
||||
<HStack className='mb-4 px-2' space={2}>
|
||||
<IconButton iconClassName='h-5 w-5' src={require('@tabler/icons/x.svg')} onClick={handleCloseClick} />
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='remote_timeline.filter_message'
|
||||
defaultMessage='You are viewing the timeline of {instance}.'
|
||||
values={{ instance }}
|
||||
/>
|
||||
</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Timeline
|
||||
scrollKey={`${timelineId}_${instance}_timeline`}
|
||||
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
|
||||
|
|
|
@ -3,9 +3,9 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
|
||||
import { fetchAccount } from 'soapbox/actions/accounts';
|
||||
import { addToMentions, removeFromMentions } from 'soapbox/actions/compose';
|
||||
import Avatar from 'soapbox/components/avatar';
|
||||
import DisplayName from 'soapbox/components/display-name';
|
||||
import AccountComponent from 'soapbox/components/account';
|
||||
import IconButton from 'soapbox/components/icon-button';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector, useCompose } from 'soapbox/hooks';
|
||||
import { makeGetAccount } from 'soapbox/selectors';
|
||||
|
||||
|
@ -44,24 +44,18 @@ const Account: React.FC<IAccount> = ({ composeId, accountId, author }) => {
|
|||
let button;
|
||||
|
||||
if (added) {
|
||||
button = <IconButton src={require('@tabler/icons/x.svg')} title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
button = <IconButton src={require('@tabler/icons/x.svg')} iconClassName='h-5 w-5' title={intl.formatMessage(messages.remove)} onClick={onRemove} />;
|
||||
} else {
|
||||
button = <IconButton src={require('@tabler/icons/plus.svg')} title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
button = <IconButton src={require('@tabler/icons/plus.svg')} iconClassName='h-5 w-5' title={intl.formatMessage(messages.add)} onClick={onAdd} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='account'>
|
||||
<div className='account__wrapper'>
|
||||
<div className='account__display-name'>
|
||||
<div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div>
|
||||
<DisplayName account={account} />
|
||||
</div>
|
||||
|
||||
<div className='account__relationship'>
|
||||
{!author && button}
|
||||
</div>
|
||||
<HStack space={1} alignItems='center' justifyContent='between' className='p-2.5'>
|
||||
<div className='w-full'>
|
||||
<AccountComponent account={account} withRelationship={false} />
|
||||
</div>
|
||||
</div>
|
||||
{!author && button}
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -88,9 +88,11 @@ const Settings = () => {
|
|||
<ListItem label={intl.formatMessage(messages.changeEmail)} onClick={navigateToChangeEmail} />
|
||||
<ListItem label={intl.formatMessage(messages.changePassword)} onClick={navigateToChangePassword} />
|
||||
<ListItem label={intl.formatMessage(messages.configureMfa)} onClick={navigateToMfa}>
|
||||
{isMfaEnabled ?
|
||||
intl.formatMessage(messages.mfaEnabled) :
|
||||
intl.formatMessage(messages.mfaDisabled)}
|
||||
<span>
|
||||
{isMfaEnabled ?
|
||||
intl.formatMessage(messages.mfaEnabled) :
|
||||
intl.formatMessage(messages.mfaDisabled)}
|
||||
</span>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
|
@ -130,14 +132,15 @@ const Settings = () => {
|
|||
|
||||
<CardBody>
|
||||
<List>
|
||||
{features.security && (
|
||||
<ListItem label={intl.formatMessage(messages.deleteAccount)} onClick={navigateToDeleteAccount} />
|
||||
)}
|
||||
{features.federating && (features.accountMoving ? (
|
||||
<ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} />
|
||||
) : features.accountAliases && (
|
||||
<ListItem label={intl.formatMessage(messages.accountAliases)} onClick={navigateToAliases} />
|
||||
))}
|
||||
|
||||
{features.security && (
|
||||
<ListItem label={intl.formatMessage(messages.deleteAccount)} onClick={navigateToDeleteAccount} />
|
||||
)}
|
||||
</List>
|
||||
</CardBody>
|
||||
</>
|
||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
|||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { HStack, Input } from 'soapbox/components/ui';
|
||||
import { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import type { CryptoAddress } from 'soapbox/types/soapbox';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react';
|
|||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { HStack, Input } from 'soapbox/components/ui';
|
||||
import { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import type { FooterItem } from 'soapbox/types/soapbox';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -2,10 +2,10 @@ import React from 'react';
|
|||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { HStack, Input } from 'soapbox/components/ui';
|
||||
import { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
|
||||
import IconPicker from './icon-picker';
|
||||
|
||||
import type { StreamfieldComponent } from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import type { PromoPanelItem } from 'soapbox/types/soapbox';
|
||||
|
||||
const messages = defineMessages({
|
||||
|
|
|
@ -8,19 +8,19 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
Column,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
FileInput,
|
||||
Form,
|
||||
FormActions,
|
||||
FormGroup,
|
||||
Input,
|
||||
FileInput,
|
||||
Streamfield,
|
||||
Textarea,
|
||||
Button,
|
||||
Toggle,
|
||||
} from 'soapbox/components/ui';
|
||||
import Streamfield from 'soapbox/components/ui/streamfield/streamfield';
|
||||
import ThemeSelector from 'soapbox/features/ui/components/theme-selector';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { normalizeSoapboxConfig } from 'soapbox/normalizers';
|
||||
|
@ -202,7 +202,7 @@ const SoapboxConfig: React.FC = () => {
|
|||
>
|
||||
<FileInput
|
||||
onChange={handleFileChange(['logo'])}
|
||||
accept='image/svg,image/png'
|
||||
accept='image/svg+xml,image/png'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import { spring } from 'react-motion';
|
|||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import StatusContent from 'soapbox/components/status-content';
|
||||
import { Stack } from 'soapbox/components/ui';
|
||||
import { HStack, Stack } from 'soapbox/components/ui';
|
||||
import AccountContainer from 'soapbox/containers/account-container';
|
||||
|
||||
import Motion from '../../util/optional-motion';
|
||||
|
@ -29,23 +29,24 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
|||
const { icon = null, text, meta = null, active = false, href = '#', isLogout, destructive } = action;
|
||||
|
||||
const Comp = href === '#' ? 'button' : 'a';
|
||||
const compProps = href === '#' ? { onClick: onClick } : { href: href };
|
||||
const compProps = href === '#' ? { onClick: onClick } : { href: href, rel: 'noopener' };
|
||||
|
||||
return (
|
||||
<li key={`${text}-${i}`}>
|
||||
<Comp
|
||||
<HStack
|
||||
{...compProps}
|
||||
rel='noopener'
|
||||
space={2.5}
|
||||
data-index={i}
|
||||
className={classNames('w-full', { active, destructive })}
|
||||
data-method={isLogout ? 'delete' : null}
|
||||
element={Comp}
|
||||
>
|
||||
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
<div>{meta}</div>
|
||||
</div>
|
||||
</Comp>
|
||||
</HStack>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -30,7 +30,7 @@ const BoostModal: React.FC<IBoostModal> = ({ status, onReblog, onClose }) => {
|
|||
|
||||
return (
|
||||
<Modal
|
||||
title='Repost?'
|
||||
title={<FormattedMessage id='boost_modal.title' defaultMessage='Repost?' />}
|
||||
confirmationAction={handleReblog}
|
||||
confirmationText={intl.formatMessage(buttonText)}
|
||||
>
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
import { Map as ImmutableMap } from 'immutable';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import { updateMrf } from 'soapbox/actions/mrf';
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { HStack, Modal, Stack, Text } from 'soapbox/components/ui';
|
||||
import { SimpleForm } from 'soapbox/features/forms';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Modal } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
|
||||
import { makeGetRemoteInstance } from 'soapbox/selectors';
|
||||
|
||||
const getRemoteInstance = makeGetRemoteInstance();
|
||||
|
||||
const messages = defineMessages({
|
||||
mediaRemoval: { id: 'edit_federation.media_removal', defaultMessage: 'Strip media' },
|
||||
forceNsfw: { id: 'edit_federation.force_nsfw', defaultMessage: 'Force attachments to be marked sensitive' },
|
||||
|
@ -31,6 +29,7 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
|
|||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const getRemoteInstance = useCallback(makeGetRemoteInstance(), []);
|
||||
const remoteInstance = useAppSelector(state => getRemoteInstance(state, host));
|
||||
|
||||
const [data, setData] = useState(ImmutableMap<string, any>());
|
||||
|
@ -82,74 +81,56 @@ const EditFederationModal: React.FC<IEditFederationModal> = ({ host, onClose })
|
|||
confirmationAction={handleSubmit}
|
||||
confirmationText={intl.formatMessage(messages.save)}
|
||||
>
|
||||
<SimpleForm onSubmit={handleSubmit}>
|
||||
<Stack space={2}>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Toggle
|
||||
checked={reject}
|
||||
onChange={handleDataChange('reject')}
|
||||
icons={false}
|
||||
id='reject'
|
||||
/>
|
||||
<List>
|
||||
<ListItem label={<FormattedMessage id='edit_federation.reject' defaultMessage='Reject all activities' />}>
|
||||
<Toggle
|
||||
checked={reject}
|
||||
onChange={handleDataChange('reject')}
|
||||
icons={false}
|
||||
id='reject'
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Text theme='muted' tag='label' size='sm' htmlFor='reject'>
|
||||
<FormattedMessage id='edit_federation.reject' defaultMessage='Reject all activities' />
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Toggle
|
||||
checked={fullMediaRemoval}
|
||||
onChange={handleMediaRemoval}
|
||||
icons={false}
|
||||
id='media_removal'
|
||||
disabled={reject}
|
||||
/>
|
||||
<ListItem label={<FormattedMessage id='edit_federation.media_removal' defaultMessage='Strip media' />}>
|
||||
<Toggle
|
||||
checked={fullMediaRemoval}
|
||||
onChange={handleMediaRemoval}
|
||||
icons={false}
|
||||
id='media_removal'
|
||||
disabled={reject}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Text theme='muted' tag='label' size='sm' htmlFor='media_removal'>
|
||||
<FormattedMessage id='edit_federation.media_removal' defaultMessage='Strip media' />
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Toggle
|
||||
checked={media_nsfw}
|
||||
onChange={handleDataChange('media_nsfw')}
|
||||
icons={false}
|
||||
id='media_nsfw'
|
||||
disabled={reject || media_removal}
|
||||
/>
|
||||
<ListItem label={<FormattedMessage id='edit_federation.force_nsfw' defaultMessage='Force attachments to be marked sensitive' />}>
|
||||
<Toggle
|
||||
checked={media_nsfw}
|
||||
onChange={handleDataChange('media_nsfw')}
|
||||
icons={false}
|
||||
id='media_nsfw'
|
||||
disabled={reject || media_removal}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Text theme='muted' tag='label' size='sm' htmlFor='media_nsfw'>
|
||||
<FormattedMessage id='edit_federation.force_nsfw' defaultMessage='Force attachments to be marked sensitive' />
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Toggle
|
||||
checked={followers_only}
|
||||
onChange={handleDataChange('followers_only')}
|
||||
icons={false}
|
||||
id='followers_only'
|
||||
disabled={reject}
|
||||
/>
|
||||
<ListItem label={<FormattedMessage id='edit_federation.followers_only' defaultMessage='Hide posts except to followers' />}>
|
||||
<Toggle
|
||||
checked={followers_only}
|
||||
onChange={handleDataChange('followers_only')}
|
||||
icons={false}
|
||||
id='followers_only'
|
||||
disabled={reject}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
<Text theme='muted' tag='label' size='sm' htmlFor='followers_only'>
|
||||
<FormattedMessage id='edit_federation.followers_only' defaultMessage='Hide posts except to followers' />
|
||||
</Text>
|
||||
</HStack>
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Toggle
|
||||
checked={federated_timeline_removal}
|
||||
onChange={handleDataChange('federated_timeline_removal')}
|
||||
icons={false}
|
||||
id='federated_timeline_removal'
|
||||
disabled={reject || followers_only}
|
||||
/>
|
||||
|
||||
<Text theme='muted' tag='label' size='sm' htmlFor='federated_timeline_removal'>
|
||||
<FormattedMessage id='edit_federation.unlisted' defaultMessage='Force posts unlisted' />
|
||||
</Text>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</SimpleForm>
|
||||
<ListItem label={<FormattedMessage id='edit_federation.unlisted' defaultMessage='Force posts unlisted' />}>
|
||||
<Toggle
|
||||
checked={federated_timeline_removal}
|
||||
onChange={handleDataChange('federated_timeline_removal')}
|
||||
icons={false}
|
||||
id='federated_timeline_removal'
|
||||
disabled={reject || followers_only}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -83,7 +83,7 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
}, [index]);
|
||||
|
||||
const getIndex = () => {
|
||||
return index !== null ? index : props.index;
|
||||
|
@ -197,7 +197,6 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
|||
width={width}
|
||||
height={height}
|
||||
startTime={time}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
link={link}
|
||||
alt={attachment.description}
|
||||
|
|
|
@ -108,7 +108,7 @@ const ReasonStep = (_props: IReasonStep) => {
|
|||
data-testid={`rule-${rule.id}`}
|
||||
onClick={() => dispatch(changeReportRule(rule.id))}
|
||||
className={classNames({
|
||||
'relative border border-solid border-gray-200 dark:border-gray-800 hover:bg-gray-100 dark:hover:bg-primary-800/30 text-left w-full p-4 flex justify-between items-center cursor-pointer': true,
|
||||
'relative border border-solid border-gray-200 dark:border-gray-800 hover:bg-gray-100 dark:hover:bg-primary-800/30 text-start w-full p-4 flex justify-between items-center cursor-pointer': true,
|
||||
'rounded-tl-lg rounded-tr-lg': idx === 0,
|
||||
'rounded-bl-lg rounded-br-lg': idx === rules.length - 1,
|
||||
'bg-gray-200 hover:bg-gray-200 dark:bg-primary-800/50': isSelected,
|
||||
|
|
|
@ -150,7 +150,7 @@ const VerifySmsModal: React.FC<IVerifySmsModal> = ({ onClose }) => {
|
|||
);
|
||||
case Statuses.READY:
|
||||
return (
|
||||
<FormGroup labelText='Phone Number'>
|
||||
<FormGroup labelText={<FormattedMessage id='sms_verification.phone.label' defaultMessage='Phone number' />}>
|
||||
<PhoneInput
|
||||
value={phone}
|
||||
onChange={onChange}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue