kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge remote-tracking branch 'origin/develop' into embedded-status
commit
c4849ad38d
|
@ -0,0 +1,55 @@
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import * as BuildConfig from 'soapbox/build_config';
|
||||||
|
import { isURL } from 'soapbox/utils/auth';
|
||||||
|
import sourceCode from 'soapbox/utils/code';
|
||||||
|
import { getFeatures } from 'soapbox/utils/features';
|
||||||
|
|
||||||
|
import { createApp } from './apps';
|
||||||
|
|
||||||
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
|
|
||||||
|
const createProviderApp = () => {
|
||||||
|
return async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const state = getState();
|
||||||
|
const { scopes } = getFeatures(state.instance);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
client_name: sourceCode.displayName,
|
||||||
|
redirect_uris: `${window.location.origin}/login/external`,
|
||||||
|
website: sourceCode.homepage,
|
||||||
|
scopes,
|
||||||
|
};
|
||||||
|
|
||||||
|
return dispatch(createApp(params));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const prepareRequest = (provider: string) => {
|
||||||
|
return async(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
|
const baseURL = isURL(BuildConfig.BACKEND_URL) ? BuildConfig.BACKEND_URL : '';
|
||||||
|
|
||||||
|
const state = getState();
|
||||||
|
const { scopes } = getFeatures(state.instance);
|
||||||
|
const app = await dispatch(createProviderApp());
|
||||||
|
const { client_id, redirect_uri } = app;
|
||||||
|
|
||||||
|
localStorage.setItem('soapbox:external:app', JSON.stringify(app));
|
||||||
|
localStorage.setItem('soapbox:external:baseurl', baseURL);
|
||||||
|
localStorage.setItem('soapbox:external:scopes', scopes);
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
provider,
|
||||||
|
authorization: {
|
||||||
|
client_id,
|
||||||
|
redirect_uri,
|
||||||
|
scope: scopes,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formdata = axios.toFormData(params);
|
||||||
|
const query = new URLSearchParams(formdata as any);
|
||||||
|
|
||||||
|
location.href = `${baseURL}/oauth/prepare_request?${query.toString()}`;
|
||||||
|
};
|
||||||
|
};
|
|
@ -37,7 +37,7 @@ if (!HTMLCanvasElement.prototype.toBlob) {
|
||||||
const dataURL = this.toDataURL(type, quality);
|
const dataURL = this.toDataURL(type, quality);
|
||||||
let data;
|
let data;
|
||||||
|
|
||||||
if (dataURL.indexOf(BASE64_MARKER) >= 0) {
|
if (dataURL.includes(BASE64_MARKER)) {
|
||||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
const [, base64] = dataURL.split(BASE64_MARKER);
|
||||||
data = decodeBase64(base64);
|
data = decodeBase64(base64);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -30,7 +30,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number, searchToke
|
||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || searchTokens.indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || !searchTokens.includes(word[0])) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ const textAtCursorMatchesToken = (str: string, caretPosition: number) => {
|
||||||
word = str.slice(left, right + caretPosition);
|
word = str.slice(left, right + caretPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!word || word.trim().length < 3 || ['@', ':', '#'].indexOf(word[0]) === -1) {
|
if (!word || word.trim().length < 3 || !['@', ':', '#'].includes(word[0])) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -442,7 +442,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
action: handleChatClick,
|
action: handleChatClick,
|
||||||
icon: require('@tabler/icons/messages.svg'),
|
icon: require('@tabler/icons/messages.svg'),
|
||||||
});
|
});
|
||||||
} else {
|
} else if (features.privacyScopes) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.direct, { name: username }),
|
text: intl.formatMessage(messages.direct, { name: username }),
|
||||||
action: handleDirectClick,
|
action: handleDirectClick,
|
||||||
|
|
|
@ -48,6 +48,7 @@ export interface IStatus {
|
||||||
hideActionBar?: boolean,
|
hideActionBar?: boolean,
|
||||||
hoverable?: boolean,
|
hoverable?: boolean,
|
||||||
variant?: 'default' | 'rounded',
|
variant?: 'default' | 'rounded',
|
||||||
|
withDismiss?: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
const Status: React.FC<IStatus> = (props) => {
|
const Status: React.FC<IStatus> = (props) => {
|
||||||
|
@ -65,6 +66,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
group,
|
group,
|
||||||
hideActionBar,
|
hideActionBar,
|
||||||
variant = 'rounded',
|
variant = 'rounded',
|
||||||
|
withDismiss,
|
||||||
} = props;
|
} = props;
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
@ -181,7 +183,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!status) return null;
|
if (!status) return null;
|
||||||
let prepend, rebloggedByText, reblogElement, reblogElementMobile;
|
let rebloggedByText, reblogElement, reblogElementMobile;
|
||||||
|
|
||||||
if (hidden) {
|
if (hidden) {
|
||||||
return (
|
return (
|
||||||
|
@ -207,20 +209,6 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featured) {
|
|
||||||
prepend = (
|
|
||||||
<div className='pt-4 px-4'>
|
|
||||||
<HStack alignItems='center' space={1}>
|
|
||||||
<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />
|
|
||||||
|
|
||||||
<Text size='sm' theme='muted' weight='medium'>
|
|
||||||
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
|
||||||
</Text>
|
|
||||||
</HStack>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.reblog && typeof status.reblog === 'object') {
|
if (status.reblog && typeof status.reblog === 'object') {
|
||||||
const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) };
|
const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) };
|
||||||
|
|
||||||
|
@ -318,7 +306,17 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
onClick={() => history.push(statusUrl)}
|
onClick={() => history.push(statusUrl)}
|
||||||
role='link'
|
role='link'
|
||||||
>
|
>
|
||||||
{prepend}
|
{featured && (
|
||||||
|
<div className='pt-4 px-4'>
|
||||||
|
<HStack alignItems='center' space={1}>
|
||||||
|
<Icon src={require('@tabler/icons/pinned.svg')} className='text-gray-600 dark:text-gray-400' />
|
||||||
|
|
||||||
|
<Text size='sm' theme='muted' weight='medium'>
|
||||||
|
<FormattedMessage id='status.pinned' defaultMessage='Pinned post' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
variant={variant}
|
variant={variant}
|
||||||
|
@ -377,7 +375,7 @@ const Status: React.FC<IStatus> = (props) => {
|
||||||
|
|
||||||
{!hideActionBar && (
|
{!hideActionBar && (
|
||||||
<div className='pt-4'>
|
<div className='pt-4'>
|
||||||
<StatusActionBar status={actualStatus} />
|
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -122,7 +122,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
|
|
||||||
const renderStatus = (statusId: string) => {
|
const renderStatus = (statusId: string) => {
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={statusId}
|
key={statusId}
|
||||||
id={statusId}
|
id={statusId}
|
||||||
|
@ -157,7 +156,6 @@ const StatusList: React.FC<IStatusList> = ({
|
||||||
if (!featuredStatusIds) return [];
|
if (!featuredStatusIds) return [];
|
||||||
|
|
||||||
return featuredStatusIds.toArray().map(statusId => (
|
return featuredStatusIds.toArray().map(statusId => (
|
||||||
// @ts-ignore
|
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
key={`f-${statusId}`}
|
key={`f-${statusId}`}
|
||||||
id={statusId}
|
id={statusId}
|
||||||
|
|
|
@ -12,12 +12,14 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
/** Text to display next ot the button. */
|
/** Text to display next ot the button. */
|
||||||
text?: string,
|
text?: string,
|
||||||
/** Don't render a background behind the icon. */
|
/** Don't render a background behind the icon. */
|
||||||
transparent?: boolean
|
transparent?: boolean,
|
||||||
|
/** Predefined styles to display for the button. */
|
||||||
|
theme?: 'seamless' | 'outlined',
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A clickable icon. */
|
/** A clickable icon. */
|
||||||
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
|
const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element => {
|
||||||
const { src, className, iconClassName, text, transparent = false, ...filteredProps } = props;
|
const { src, className, iconClassName, text, transparent = false, theme = 'seamless', ...filteredProps } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -25,6 +27,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
|
||||||
type='button'
|
type='button'
|
||||||
className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', {
|
className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', {
|
||||||
'bg-white dark:bg-transparent': !transparent,
|
'bg-white dark:bg-transparent': !transparent,
|
||||||
|
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
||||||
'opacity-50': filteredProps.disabled,
|
'opacity-50': filteredProps.disabled,
|
||||||
}, className)}
|
}, className)}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
|
|
|
@ -4,15 +4,13 @@ import Status, { IStatus } from 'soapbox/components/status';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
import { makeGetStatus } from 'soapbox/selectors';
|
import { makeGetStatus } from 'soapbox/selectors';
|
||||||
|
|
||||||
interface IStatusContainer extends Omit<IStatus, 'id'> {
|
interface IStatusContainer extends Omit<IStatus, 'status'> {
|
||||||
id: string,
|
id: string,
|
||||||
/** @deprecated Unused. */
|
/** @deprecated Unused. */
|
||||||
contextType?: any,
|
contextType?: any,
|
||||||
/** @deprecated Unused. */
|
/** @deprecated Unused. */
|
||||||
otherAccounts?: any,
|
otherAccounts?: any,
|
||||||
/** @deprecated Unused. */
|
/** @deprecated Unused. */
|
||||||
withDismiss?: any,
|
|
||||||
/** @deprecated Unused. */
|
|
||||||
getScrollPosition?: any,
|
getScrollPosition?: any,
|
||||||
/** @deprecated Unused. */
|
/** @deprecated Unused. */
|
||||||
updateScrollBottom?: any,
|
updateScrollBottom?: any,
|
||||||
|
@ -24,11 +22,12 @@ const getStatus = makeGetStatus();
|
||||||
* Legacy Status wrapper accepting a status ID instead of the full entity.
|
* Legacy Status wrapper accepting a status ID instead of the full entity.
|
||||||
* @deprecated Use the Status component directly.
|
* @deprecated Use the Status component directly.
|
||||||
*/
|
*/
|
||||||
const StatusContainer: React.FC<IStatusContainer> = ({ id }) => {
|
const StatusContainer: React.FC<IStatusContainer> = (props) => {
|
||||||
|
const { id, ...rest } = props;
|
||||||
const status = useAppSelector(state => getStatus(state, { id }));
|
const status = useAppSelector(state => getStatus(state, { id }));
|
||||||
|
|
||||||
if (status) {
|
if (status) {
|
||||||
return <Status status={status} />;
|
return <Status status={status} {...rest} />;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -696,7 +696,8 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
// src={require('@tabler/icons/mail.svg')}
|
// src={require('@tabler/icons/mail.svg')}
|
||||||
// onClick={onDirect}
|
// onClick={onDirect}
|
||||||
// title={intl.formatMessage(messages.direct, { name: account.username })}
|
// title={intl.formatMessage(messages.direct, { name: account.username })}
|
||||||
// className='px-2 border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500'
|
// theme='outlined'
|
||||||
|
// className='px-2'
|
||||||
// iconClassName='w-4 h-4'
|
// iconClassName='w-4 h-4'
|
||||||
// />
|
// />
|
||||||
// );
|
// );
|
||||||
|
@ -715,7 +716,8 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
src={require('@tabler/icons/upload.svg')}
|
src={require('@tabler/icons/upload.svg')}
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
title={intl.formatMessage(messages.share, { name: account.username })}
|
title={intl.formatMessage(messages.share, { name: account.username })}
|
||||||
className='px-2 border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500'
|
theme='outlined'
|
||||||
|
className='px-2'
|
||||||
iconClassName='w-4 h-4'
|
iconClassName='w-4 h-4'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -770,7 +772,8 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
src={require('@tabler/icons/dots.svg')}
|
src={require('@tabler/icons/dots.svg')}
|
||||||
className='px-2 border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500'
|
theme='outlined'
|
||||||
|
className='px-2'
|
||||||
iconClassName='w-4 h-4'
|
iconClassName='w-4 h-4'
|
||||||
children={null}
|
children={null}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -38,7 +38,7 @@ const MediaItem: React.FC<IMediaItem> = ({ attachment, displayWidth, onOpenMedia
|
||||||
};
|
};
|
||||||
|
|
||||||
const hoverToPlay = () => {
|
const hoverToPlay = () => {
|
||||||
return !autoPlayGif && ['gifv', 'video'].indexOf(attachment.type) !== -1;
|
return !autoPlayGif && ['gifv', 'video'].includes(attachment.type);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick: React.MouseEventHandler = e => {
|
const handleClick: React.MouseEventHandler = e => {
|
||||||
|
|
|
@ -65,7 +65,7 @@ const AccountTimeline: React.FC<IAccountTimeline> = ({ params, withReplies = fal
|
||||||
if (account) {
|
if (account) {
|
||||||
dispatch(expandAccountTimeline(account.id, { withReplies }));
|
dispatch(expandAccountTimeline(account.id, { withReplies }));
|
||||||
}
|
}
|
||||||
}, [account?.id]);
|
}, [account?.id, withReplies]);
|
||||||
|
|
||||||
const handleLoadMore = (maxId: string) => {
|
const handleLoadMore = (maxId: string) => {
|
||||||
if (account) {
|
if (account) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { normalizeInstance } from 'soapbox/normalizers';
|
||||||
|
|
||||||
import { fireEvent, render, screen } from '../../../../jest/test-helpers';
|
import { fireEvent, render, screen } from '../../../../jest/test-helpers';
|
||||||
import LoginForm from '../login_form';
|
import LoginForm from '../login_form';
|
||||||
|
|
||||||
|
@ -8,7 +9,7 @@ describe('<LoginForm />', () => {
|
||||||
it('renders for Pleroma', () => {
|
it('renders for Pleroma', () => {
|
||||||
const mockFn = jest.fn();
|
const mockFn = jest.fn();
|
||||||
const store = {
|
const store = {
|
||||||
instance: ImmutableMap({
|
instance: normalizeInstance({
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
@ -21,7 +22,7 @@ describe('<LoginForm />', () => {
|
||||||
it('renders for Mastodon', () => {
|
it('renders for Mastodon', () => {
|
||||||
const mockFn = jest.fn();
|
const mockFn = jest.fn();
|
||||||
const store = {
|
const store = {
|
||||||
instance: ImmutableMap({
|
instance: normalizeInstance({
|
||||||
version: '3.0.0',
|
version: '3.0.0',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Map as ImmutableMap } from 'immutable';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { normalizeInstance } from 'soapbox/normalizers';
|
||||||
|
|
||||||
import { render, screen } from '../../../../jest/test-helpers';
|
import { render, screen } from '../../../../jest/test-helpers';
|
||||||
import LoginPage from '../login_page';
|
import LoginPage from '../login_page';
|
||||||
|
|
||||||
describe('<LoginPage />', () => {
|
describe('<LoginPage />', () => {
|
||||||
it('renders correctly on load', () => {
|
it('renders correctly on load', () => {
|
||||||
const store = {
|
const store = {
|
||||||
instance: ImmutableMap({
|
instance: normalizeInstance({
|
||||||
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
version: '2.7.2 (compatible; Pleroma 2.3.0)',
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
import { prepareRequest } from 'soapbox/actions/consumer-auth';
|
||||||
|
import { IconButton, Tooltip } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch } from 'soapbox/hooks';
|
||||||
|
import { capitalize } from 'soapbox/utils/strings';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
tooltip: { id: 'oauth_consumer.tooltip', defaultMessage: 'Sign in with {provider}' },
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Map between OAuth providers and brand icons. */
|
||||||
|
const BRAND_ICONS: Record<string, string> = {
|
||||||
|
twitter: require('@tabler/icons/brand-twitter.svg'),
|
||||||
|
facebook: require('@tabler/icons/brand-facebook.svg'),
|
||||||
|
google: require('@tabler/icons/brand-google.svg'),
|
||||||
|
microsoft: require('@tabler/icons/brand-windows.svg'),
|
||||||
|
slack: require('@tabler/icons/brand-slack.svg'),
|
||||||
|
github: require('@tabler/icons/brand-github.svg'),
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IConsumerButton {
|
||||||
|
provider: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/** OAuth consumer button for logging in with a third-party service. */
|
||||||
|
const ConsumerButton: React.FC<IConsumerButton> = ({ provider }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const icon = BRAND_ICONS[provider] || require('@tabler/icons/key.svg');
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
dispatch(prepareRequest(provider));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip text={intl.formatMessage(messages.tooltip, { provider: capitalize(provider) })}>
|
||||||
|
<IconButton
|
||||||
|
theme='outlined'
|
||||||
|
className='p-2.5'
|
||||||
|
iconClassName='w-6 h-6'
|
||||||
|
src={icon}
|
||||||
|
onClick={handleClick}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConsumerButton;
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
import React from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { Card, HStack, Text } from 'soapbox/components/ui';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import ConsumerButton from './consumer-button';
|
||||||
|
|
||||||
|
interface IConsumersList {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays OAuth consumers to log in with. */
|
||||||
|
const ConsumersList: React.FC<IConsumersList> = () => {
|
||||||
|
const providers = useAppSelector(state => ImmutableList<string>(state.instance.pleroma.get('oauth_consumer_strategies')));
|
||||||
|
|
||||||
|
if (providers.size > 0) {
|
||||||
|
return (
|
||||||
|
<Card className='p-4 sm:rounded-xl bg-gray-50 dark:bg-primary-800'>
|
||||||
|
<Text size='xs' theme='muted'>
|
||||||
|
<FormattedMessage id='oauth_consumers.title' defaultMessage='Other ways to sign in' />
|
||||||
|
</Text>
|
||||||
|
<HStack space={2}>
|
||||||
|
{providers.map(provider => (
|
||||||
|
<ConsumerButton provider={provider} />
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConsumersList;
|
|
@ -2,7 +2,9 @@ import React from 'react';
|
||||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { Button, Form, FormActions, FormGroup, Input } from 'soapbox/components/ui';
|
import { Button, Form, FormActions, FormGroup, Input, Stack } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
import ConsumersList from './consumers-list';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
username: {
|
username: {
|
||||||
|
@ -29,7 +31,7 @@ const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
<h1 className='text-center font-bold text-2xl'><FormattedMessage id='login_form.header' defaultMessage='Sign In' /></h1>
|
<h1 className='text-center font-bold text-2xl'><FormattedMessage id='login_form.header' defaultMessage='Sign In' /></h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto'>
|
<Stack className='sm:pt-10 sm:w-2/3 md:w-1/2 mx-auto' space={5}>
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit}>
|
||||||
<FormGroup labelText={intl.formatMessage(messages.username)}>
|
<FormGroup labelText={intl.formatMessage(messages.username)}>
|
||||||
<Input
|
<Input
|
||||||
|
@ -76,7 +78,9 @@ const LoginForm: React.FC<ILoginForm> = ({ isLoading, handleSubmit }) => {
|
||||||
</Button>
|
</Button>
|
||||||
</FormActions>
|
</FormActions>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
|
||||||
|
<ConsumersList />
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -85,8 +85,8 @@ export function search(value, { emojisToShowFilter, maxResults, include, exclude
|
||||||
pool = {};
|
pool = {};
|
||||||
|
|
||||||
data.categories.forEach(category => {
|
data.categories.forEach(category => {
|
||||||
const isIncluded = include && include.length ? include.indexOf(category.name.toLowerCase()) > -1 : true;
|
const isIncluded = include && include.length ? include.includes(category.name.toLowerCase()) : true;
|
||||||
const isExcluded = exclude && exclude.length ? exclude.indexOf(category.name.toLowerCase()) > -1 : false;
|
const isExcluded = exclude && exclude.length ? exclude.includes(category.name.toLowerCase()) : false;
|
||||||
if (!isIncluded || isExcluded) {
|
if (!isIncluded || isExcluded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -95,8 +95,8 @@ export function search(value, { emojisToShowFilter, maxResults, include, exclude
|
||||||
});
|
});
|
||||||
|
|
||||||
if (custom.length) {
|
if (custom.length) {
|
||||||
const customIsIncluded = include && include.length ? include.indexOf('custom') > -1 : true;
|
const customIsIncluded = include && include.length ? include.includes('custom') : true;
|
||||||
const customIsExcluded = exclude && exclude.length ? exclude.indexOf('custom') > -1 : false;
|
const customIsExcluded = exclude && exclude.length ? exclude.includes('custom') : false;
|
||||||
if (customIsIncluded && !customIsExcluded) {
|
if (customIsIncluded && !customIsExcluded) {
|
||||||
addCustomToPool(custom, pool);
|
addCustomToPool(custom, pool);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ const buildSearch = (data) => {
|
||||||
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
(split ? string.split(/[-|_|\s]+/) : [string]).forEach((s) => {
|
||||||
s = s.toLowerCase();
|
s = s.toLowerCase();
|
||||||
|
|
||||||
if (search.indexOf(s) === -1) {
|
if (!search.includes(s)) {
|
||||||
search.push(s);
|
search.push(s);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -190,7 +190,7 @@ function getData(emoji, skin, set) {
|
||||||
|
|
||||||
function uniq(arr) {
|
function uniq(arr) {
|
||||||
return arr.reduce((acc, item) => {
|
return arr.reduce((acc, item) => {
|
||||||
if (acc.indexOf(item) === -1) {
|
if (!acc.includes(item)) {
|
||||||
acc.push(item);
|
acc.push(item);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
|
@ -201,7 +201,7 @@ function intersect(a, b) {
|
||||||
const uniqA = uniq(a);
|
const uniqA = uniq(a);
|
||||||
const uniqB = uniq(b);
|
const uniqB = uniq(b);
|
||||||
|
|
||||||
return uniqA.filter(item => uniqB.indexOf(item) >= 0);
|
return uniqA.filter(item => uniqB.includes(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
function deepMerge(a, b) {
|
function deepMerge(a, b) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
||||||
import { useAppDispatch, useAppSelector, useDimensions, useFeatures } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useDimensions } from 'soapbox/hooks';
|
||||||
import useCarouselAvatars from 'soapbox/queries/carousels';
|
import useCarouselAvatars from 'soapbox/queries/carousels';
|
||||||
|
|
||||||
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
|
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
|
||||||
|
@ -59,8 +59,6 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const FeedCarousel = () => {
|
const FeedCarousel = () => {
|
||||||
const features = useFeatures();
|
|
||||||
|
|
||||||
const { data: avatars, isFetching, isError } = useCarouselAvatars();
|
const { data: avatars, isFetching, isError } = useCarouselAvatars();
|
||||||
|
|
||||||
const [cardRef, setCardRef, { width }] = useDimensions();
|
const [cardRef, setCardRef, { width }] = useDimensions();
|
||||||
|
@ -83,10 +81,6 @@ const FeedCarousel = () => {
|
||||||
}
|
}
|
||||||
}, [width, widthPerAvatar]);
|
}, [width, widthPerAvatar]);
|
||||||
|
|
||||||
if (!features.feedUserFiltering) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<Card variant='rounded' size='lg' data-testid='feed-carousel-error'>
|
<Card variant='rounded' size='lg' data-testid='feed-carousel-error'>
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { prepareRequest } from 'soapbox/actions/consumer-auth';
|
||||||
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui';
|
||||||
import VerificationBadge from 'soapbox/components/verification_badge';
|
import VerificationBadge from 'soapbox/components/verification_badge';
|
||||||
import RegistrationForm from 'soapbox/features/auth_login/components/registration_form';
|
import RegistrationForm from 'soapbox/features/auth_login/components/registration_form';
|
||||||
import { useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||||
|
import { capitalize } from 'soapbox/utils/strings';
|
||||||
|
|
||||||
const LandingPage = () => {
|
const LandingPage = () => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const features = useFeatures();
|
const features = useFeatures();
|
||||||
const soapboxConfig = useSoapboxConfig();
|
const soapboxConfig = useSoapboxConfig();
|
||||||
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
const pepeEnabled = soapboxConfig.getIn(['extensions', 'pepe', 'enabled']) === true;
|
||||||
|
@ -40,6 +43,29 @@ const LandingPage = () => {
|
||||||
return <RegistrationForm />;
|
return <RegistrationForm />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Display login button for external provider. */
|
||||||
|
const renderProvider = () => {
|
||||||
|
const { authProvider } = soapboxConfig;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={3}>
|
||||||
|
<Stack>
|
||||||
|
<Text size='2xl' weight='bold' align='center'>
|
||||||
|
<FormattedMessage id='registrations.get_started' defaultMessage="Let's get started!" />
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Button onClick={() => dispatch(prepareRequest(authProvider))} theme='primary' block>
|
||||||
|
<FormattedMessage
|
||||||
|
id='oauth_consumer.tooltip'
|
||||||
|
defaultMessage='Sign in with {provider}'
|
||||||
|
values={{ provider: capitalize(authProvider) }}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/** Pepe API registrations are open */
|
/** Pepe API registrations are open */
|
||||||
const renderPepe = () => {
|
const renderPepe = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -47,18 +73,26 @@ const LandingPage = () => {
|
||||||
<VerificationBadge className='h-16 w-16 mx-auto' />
|
<VerificationBadge className='h-16 w-16 mx-auto' />
|
||||||
|
|
||||||
<Stack>
|
<Stack>
|
||||||
<Text size='2xl' weight='bold' align='center'>Let's get started!</Text>
|
<Text size='2xl' weight='bold' align='center'>
|
||||||
<Text theme='muted' align='center'>Social Media Without Discrimination</Text>
|
<FormattedMessage id='registrations.get_started' defaultMessage="Let's get started!" />
|
||||||
|
</Text>
|
||||||
|
<Text theme='muted' align='center'>
|
||||||
|
<FormattedMessage id='registrations.tagline' defaultMessage='Social Media Without Discrimination' />
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Button to='/verify' theme='primary' block>Create an account</Button>
|
<Button to='/verify' theme='primary' block>
|
||||||
|
<FormattedMessage id='registrations.create_account' defaultMessage='Create an account' />
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Render registration flow depending on features
|
// Render registration flow depending on features
|
||||||
const renderBody = () => {
|
const renderBody = () => {
|
||||||
if (pepeEnabled && pepeOpen) {
|
if (soapboxConfig.authProvider) {
|
||||||
|
return renderProvider();
|
||||||
|
} else if (pepeEnabled && pepeOpen) {
|
||||||
return renderPepe();
|
return renderPepe();
|
||||||
} else if (features.accountCreation && instance.registrations) {
|
} else if (features.accountCreation && instance.registrations) {
|
||||||
return renderOpen();
|
return renderOpen();
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { makeGetNotification } from 'soapbox/selectors';
|
||||||
import { NotificationType, validType } from 'soapbox/utils/notification';
|
import { NotificationType, validType } from 'soapbox/utils/notification';
|
||||||
|
|
||||||
import type { ScrollPosition } from 'soapbox/components/status';
|
import type { ScrollPosition } from 'soapbox/components/status';
|
||||||
import type { Account, Status, Notification as NotificationEntity } from 'soapbox/types/entities';
|
import type { Account, Status as StatusEntity, Notification as NotificationEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const getNotification = makeGetNotification();
|
const getNotification = makeGetNotification();
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
id: 'notification.update',
|
id: 'notification.update',
|
||||||
defaultMessage: '{name} edited a post',
|
defaultMessage: '{name} edited a post you interacted with',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ interface INotificaton {
|
||||||
notification: NotificationEntity,
|
notification: NotificationEntity,
|
||||||
onMoveUp?: (notificationId: string) => void,
|
onMoveUp?: (notificationId: string) => void,
|
||||||
onMoveDown?: (notificationId: string) => void,
|
onMoveDown?: (notificationId: string) => void,
|
||||||
onReblog?: (status: Status, e?: KeyboardEvent) => void,
|
onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void,
|
||||||
getScrollPosition?: () => ScrollPosition | undefined,
|
getScrollPosition?: () => ScrollPosition | undefined,
|
||||||
updateScrollBottom?: (bottom: number) => void,
|
updateScrollBottom?: (bottom: number) => void,
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
if (e?.shiftKey || !boostModal) {
|
if (e?.shiftKey || !boostModal) {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status));
|
||||||
} else {
|
} else {
|
||||||
dispatch(openModal('BOOST', { status, onReblog: (status: Status) => {
|
dispatch(openModal('BOOST', { status, onReblog: (status: StatusEntity) => {
|
||||||
dispatch(reblog(status));
|
dispatch(reblog(status));
|
||||||
} }));
|
} }));
|
||||||
}
|
}
|
||||||
|
@ -303,16 +303,12 @@ const Notification: React.FC<INotificaton> = (props) => {
|
||||||
case 'update':
|
case 'update':
|
||||||
case 'pleroma:emoji_reaction':
|
case 'pleroma:emoji_reaction':
|
||||||
return status && typeof status === 'object' ? (
|
return status && typeof status === 'object' ? (
|
||||||
// @ts-ignore
|
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
id={status.id}
|
id={status.id}
|
||||||
withDismiss
|
withDismiss
|
||||||
hidden={hidden}
|
hidden={hidden}
|
||||||
onMoveDown={handleMoveDown}
|
onMoveDown={handleMoveDown}
|
||||||
onMoveUp={handleMoveUp}
|
onMoveUp={handleMoveUp}
|
||||||
contextType='notifications'
|
|
||||||
getScrollPosition={props.getScrollPosition}
|
|
||||||
updateScrollBottom={props.updateScrollBottom}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -26,7 +26,7 @@ const addAutoPlay = (html: string): string => {
|
||||||
const iframe = document.querySelector('iframe');
|
const iframe = document.querySelector('iframe');
|
||||||
|
|
||||||
if (iframe) {
|
if (iframe) {
|
||||||
if (iframe.src.indexOf('?') !== -1) {
|
if (iframe.src.includes('?')) {
|
||||||
iframe.src += '&';
|
iframe.src += '&';
|
||||||
} else {
|
} else {
|
||||||
iframe.src += '?';
|
iframe.src += '?';
|
||||||
|
|
|
@ -5,7 +5,7 @@ import Icon from 'soapbox/components/icon';
|
||||||
import StatusMedia from 'soapbox/components/status-media';
|
import StatusMedia from 'soapbox/components/status-media';
|
||||||
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
import StatusReplyMentions from 'soapbox/components/status-reply-mentions';
|
||||||
import StatusContent from 'soapbox/components/status_content';
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
import { HStack, Text } from 'soapbox/components/ui';
|
import { HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
import AccountContainer from 'soapbox/containers/account_container';
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
import QuotedStatus from 'soapbox/features/status/containers/quoted_status_container';
|
||||||
import { getActualStatus } from 'soapbox/utils/status';
|
import { getActualStatus } from 'soapbox/utils/status';
|
||||||
|
@ -65,9 +65,9 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actualStatus.visibility === 'direct') {
|
if (actualStatus.visibility === 'direct') {
|
||||||
statusTypeIcon = <Icon src={require('@tabler/icons/mail.svg')} />;
|
statusTypeIcon = <Icon className='text-gray-700 dark:text-gray-600' src={require('@tabler/icons/mail.svg')} />;
|
||||||
} else if (actualStatus.visibility === 'private') {
|
} else if (actualStatus.visibility === 'private') {
|
||||||
statusTypeIcon = <Icon src={require('@tabler/icons/lock.svg')} />;
|
statusTypeIcon = <Icon className='text-gray-700 dark:text-gray-600' src={require('@tabler/icons/lock.svg')} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -102,7 +102,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
<HStack justifyContent='between' alignItems='center' className='py-2'>
|
<HStack justifyContent='between' alignItems='center' className='py-2'>
|
||||||
<StatusInteractionBar status={actualStatus} />
|
<StatusInteractionBar status={actualStatus} />
|
||||||
|
|
||||||
<div className='detailed-actualStatus__timestamp'>
|
<Stack space={1} alignItems='center'>
|
||||||
{statusTypeIcon}
|
{statusTypeIcon}
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
|
@ -128,7 +128,7 @@ const DetailedStatus: React.FC<IDetailedStatus> = ({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</Stack>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { useAppSelector } from 'soapbox/hooks';
|
||||||
interface IThreadStatus {
|
interface IThreadStatus {
|
||||||
id: string,
|
id: string,
|
||||||
focusedStatusId: string,
|
focusedStatusId: string,
|
||||||
|
onMoveUp: (id: string) => void,
|
||||||
|
onMoveDown: (id: string) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Status with reply-connector in threads. */
|
/** Status with reply-connector in threads. */
|
||||||
|
|
|
@ -369,6 +369,8 @@ const Thread: React.FC<IThread> = (props) => {
|
||||||
key={id}
|
key={id}
|
||||||
id={id}
|
id={id}
|
||||||
focusedStatusId={status!.id}
|
focusedStatusId={status!.id}
|
||||||
|
onMoveUp={handleMoveUp}
|
||||||
|
onMoveDown={handleMoveDown}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -92,7 +92,7 @@ export default class ModalRoot extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoading = modalId => () => {
|
renderLoading = modalId => () => {
|
||||||
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
|
return !['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].includes(modalId) ? <ModalLoading /> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError = (props) => {
|
renderError = (props) => {
|
||||||
|
|
|
@ -93,7 +93,8 @@ const SubscriptionButton = ({ account }: ISubscriptionButton) => {
|
||||||
src={isSubscribed ? require('@tabler/icons/bell-ringing.svg') : require('@tabler/icons/bell.svg')}
|
src={isSubscribed ? require('@tabler/icons/bell-ringing.svg') : require('@tabler/icons/bell.svg')}
|
||||||
onClick={handleToggle}
|
onClick={handleToggle}
|
||||||
title={title}
|
title={title}
|
||||||
className='px-2 border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500'
|
theme='outlined'
|
||||||
|
className='px-2'
|
||||||
iconClassName='w-4 h-4'
|
iconClassName='w-4 h-4'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -28,7 +28,7 @@ const UserPanel: React.FC<IUserPanel> = ({ accountId, action, badges, domain })
|
||||||
|
|
||||||
if (!account) return null;
|
if (!account) return null;
|
||||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
const acct = !account.get('acct').includes('@') && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||||
const header = account.get('header');
|
const header = account.get('header');
|
||||||
const verified = account.get('verified');
|
const verified = account.get('verified');
|
||||||
|
|
||||||
|
|
|
@ -352,7 +352,7 @@ const UI: React.FC = ({ children }) => {
|
||||||
const handleDragEnter = (e: DragEvent) => {
|
const handleDragEnter = (e: DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (e.target && dragTargets.current.indexOf(e.target) === -1) {
|
if (e.target && !dragTargets.current.includes(e.target)) {
|
||||||
dragTargets.current.push(e.target);
|
dragTargets.current.push(e.target);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import BundleContainer from '../containers/bundle_container';
|
||||||
type PageProps = {
|
type PageProps = {
|
||||||
params?: MatchType['params'],
|
params?: MatchType['params'],
|
||||||
layout?: any,
|
layout?: any,
|
||||||
|
children: React.ReactNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IWrappedRoute extends RouteProps {
|
interface IWrappedRoute extends RouteProps {
|
||||||
|
|
|
@ -85,13 +85,15 @@ const EmailVerification = () => {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((error: AxiosError) => {
|
.catch((error: AxiosError) => {
|
||||||
const isEmailTaken = (error.response?.data as any)?.error === 'email_taken';
|
const errorMessage = (error.response?.data as any)?.error;
|
||||||
|
const isEmailTaken = errorMessage === 'email_taken';
|
||||||
|
let message = intl.formatMessage({ id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' });
|
||||||
|
|
||||||
const message = isEmailTaken ? (
|
if (isEmailTaken) {
|
||||||
intl.formatMessage({ id: 'email_verification.exists', defaultMessage: 'This email has already been taken.' })
|
message = intl.formatMessage({ id: 'email_verification.exists', defaultMessage: 'This email has already been taken.' });
|
||||||
) : (
|
} else if (errorMessage) {
|
||||||
intl.formatMessage({ id: 'email_verification.fail', defaultMessage: 'Failed to request email verification.' })
|
message = errorMessage;
|
||||||
);
|
}
|
||||||
|
|
||||||
if (isEmailTaken) {
|
if (isEmailTaken) {
|
||||||
setErrors([intl.formatMessage({ id: 'email_verification.taken', defaultMessage: 'is taken' })]);
|
setErrors([intl.formatMessage({ id: 'email_verification.taken', defaultMessage: 'is taken' })]);
|
||||||
|
|
|
@ -71,6 +71,7 @@ export const CryptoAddressRecord = ImmutableRecord({
|
||||||
export const SoapboxConfigRecord = ImmutableRecord({
|
export const SoapboxConfigRecord = ImmutableRecord({
|
||||||
ads: ImmutableList<Ad>(),
|
ads: ImmutableList<Ad>(),
|
||||||
appleAppId: null,
|
appleAppId: null,
|
||||||
|
authProvider: '',
|
||||||
logo: '',
|
logo: '',
|
||||||
logoDarkMode: null,
|
logoDarkMode: null,
|
||||||
banner: '',
|
banner: '',
|
||||||
|
|
|
@ -58,7 +58,7 @@ const HomePage: React.FC = ({ children }) => {
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<FeedCarousel />
|
{features.feedUserFiltering && <FeedCarousel />}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import LinkFooter from 'soapbox/features/ui/components/link_footer';
|
|
||||||
import {
|
|
||||||
WhoToFollowPanel,
|
|
||||||
TrendsPanel,
|
|
||||||
SignUpPanel,
|
|
||||||
CtaBanner,
|
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
|
||||||
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
|
||||||
import { getFeatures } from 'soapbox/utils/features';
|
|
||||||
|
|
||||||
import { Layout } from '../components/ui';
|
|
||||||
import BundleContainer from '../features/ui/containers/bundle_container';
|
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
|
||||||
const me = state.get('me');
|
|
||||||
const features = getFeatures(state.get('instance'));
|
|
||||||
|
|
||||||
return {
|
|
||||||
me,
|
|
||||||
showTrendsPanel: features.trends,
|
|
||||||
showWhoToFollowPanel: features.suggestions,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default @connect(mapStateToProps)
|
|
||||||
class StatusPage extends ImmutablePureComponent {
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { me, children, showTrendsPanel, showWhoToFollowPanel } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Layout.Main>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{!me && (
|
|
||||||
<BundleContainer fetchComponent={CtaBanner}>
|
|
||||||
{Component => <Component key='cta-banner' />}
|
|
||||||
</BundleContainer>
|
|
||||||
)}
|
|
||||||
</Layout.Main>
|
|
||||||
|
|
||||||
<Layout.Aside>
|
|
||||||
{!me && (
|
|
||||||
<BundleContainer fetchComponent={SignUpPanel}>
|
|
||||||
{Component => <Component key='sign-up-panel' />}
|
|
||||||
</BundleContainer>
|
|
||||||
)}
|
|
||||||
{showTrendsPanel && (
|
|
||||||
<BundleContainer fetchComponent={TrendsPanel}>
|
|
||||||
{Component => <Component limit={3} key='trends-panel' />}
|
|
||||||
</BundleContainer>
|
|
||||||
)}
|
|
||||||
{showWhoToFollowPanel && (
|
|
||||||
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
|
||||||
{Component => <Component limit={5} key='wtf-panel' />}
|
|
||||||
</BundleContainer>
|
|
||||||
)}
|
|
||||||
<LinkFooter key='link-footer' />
|
|
||||||
</Layout.Aside>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import LinkFooter from 'soapbox/features/ui/components/link_footer';
|
||||||
|
import {
|
||||||
|
WhoToFollowPanel,
|
||||||
|
TrendsPanel,
|
||||||
|
SignUpPanel,
|
||||||
|
CtaBanner,
|
||||||
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
|
import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import { Layout } from '../components/ui';
|
||||||
|
import BundleContainer from '../features/ui/containers/bundle_container';
|
||||||
|
|
||||||
|
interface IStatusPage {
|
||||||
|
children: React.ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusPage: React.FC<IStatusPage> = ({ children }) => {
|
||||||
|
const me = useAppSelector(state => state.me);
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Layout.Main>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
{!me && (
|
||||||
|
<BundleContainer fetchComponent={CtaBanner}>
|
||||||
|
{Component => <Component key='cta-banner' />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
</Layout.Main>
|
||||||
|
|
||||||
|
<Layout.Aside>
|
||||||
|
{!me && (
|
||||||
|
<BundleContainer fetchComponent={SignUpPanel}>
|
||||||
|
{Component => <Component key='sign-up-panel' />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
{features.trends && (
|
||||||
|
<BundleContainer fetchComponent={TrendsPanel}>
|
||||||
|
{Component => <Component limit={3} key='trends-panel' />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
{features.suggestions && (
|
||||||
|
<BundleContainer fetchComponent={WhoToFollowPanel}>
|
||||||
|
{Component => <Component limit={5} key='wtf-panel' />}
|
||||||
|
</BundleContainer>
|
||||||
|
)}
|
||||||
|
<LinkFooter key='link-footer' />
|
||||||
|
</Layout.Aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusPage;
|
|
@ -93,7 +93,7 @@ const toServerSideType = (columnType: string): string => {
|
||||||
case 'thread':
|
case 'thread':
|
||||||
return columnType;
|
return columnType;
|
||||||
default:
|
default:
|
||||||
if (columnType.indexOf('list:') > -1) {
|
if (columnType.includes('list:')) {
|
||||||
return 'home';
|
return 'home';
|
||||||
} else {
|
} else {
|
||||||
return 'public'; // community, account, hashtag
|
return 'public'; // community, account, hashtag
|
||||||
|
|
|
@ -146,7 +146,7 @@ const handlePush = (event: PushEvent) => {
|
||||||
timestamp: notification.created_at && Number(new Date(notification.created_at)),
|
timestamp: notification.created_at && Number(new Date(notification.created_at)),
|
||||||
tag: notification.id,
|
tag: notification.id,
|
||||||
image: notification.status?.media_attachments[0]?.preview_url,
|
image: notification.status?.media_attachments[0]?.preview_url,
|
||||||
data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.username}/posts/${notification.status.id}` : `/@${notification.account.username}` },
|
data: { access_token, preferred_locale, id: notification.status ? notification.status.id : notification.account.id, url: notification.status ? `/@${notification.account.acct}/posts/${notification.status.id}` : `/@${notification.account.acct}` },
|
||||||
};
|
};
|
||||||
|
|
||||||
if (notification.status?.spoiler_text || notification.status?.sensitive) {
|
if (notification.status?.spoiler_text || notification.status?.sensitive) {
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
/** Capitalize the first letter of a string. */
|
||||||
|
// https://stackoverflow.com/a/1026087
|
||||||
|
function capitalize(str: string) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { capitalize };
|
|
@ -304,7 +304,7 @@
|
||||||
li:not(:empty) {
|
li:not(:empty) {
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
@apply flex items-center px-4 py-3 text-gray-600 dark:text-gray-300 no-underline hover:bg-gray-100 dark:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-200;
|
@apply flex items-center px-4 py-3 text-gray-600 dark:text-gray-300 no-underline hover:bg-gray-100 dark:bg-gray-800 hover:text-gray-800 dark:hover:text-gray-200 text-left;
|
||||||
|
|
||||||
&.destructive {
|
&.destructive {
|
||||||
@apply text-danger-600;
|
@apply text-danger-600;
|
||||||
|
@ -325,7 +325,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
button[type="button"] {
|
button[type="button"] {
|
||||||
@apply w-full justify-center;
|
@apply w-full justify-center text-center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,7 +66,10 @@ module.exports = [{
|
||||||
}, {
|
}, {
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
include: resolve('node_modules', '@tabler'),
|
include: [
|
||||||
|
resolve('node_modules', '@tabler'),
|
||||||
|
resolve('custom', 'modules', '@tabler'),
|
||||||
|
],
|
||||||
generator: {
|
generator: {
|
||||||
filename: 'packs/icons/[name]-[contenthash:8][ext]',
|
filename: 'packs/icons/[name]-[contenthash:8][ext]',
|
||||||
},
|
},
|
||||||
|
|
|
@ -141,6 +141,7 @@ module.exports = {
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: settings.extensions,
|
extensions: settings.extensions,
|
||||||
modules: [
|
modules: [
|
||||||
|
resolve('custom', 'modules'),
|
||||||
resolve(settings.source_path),
|
resolve(settings.source_path),
|
||||||
'node_modules',
|
'node_modules',
|
||||||
],
|
],
|
||||||
|
|
Ładowanie…
Reference in New Issue