commit
733d240893
@ -0,0 +1,95 @@
|
||||
{
|
||||
"id": "108046244464677537",
|
||||
"created_at": "2022-03-30T15:40:53.287Z",
|
||||
"in_reply_to_id": null,
|
||||
"in_reply_to_account_id": null,
|
||||
"sensitive": false,
|
||||
"spoiler_text": "",
|
||||
"visibility": "public",
|
||||
"language": null,
|
||||
"uri": "https://truthsocial.com/users/alex/statuses/108046244464677537",
|
||||
"url": "https://truthsocial.com/@alex/108046244464677537",
|
||||
"replies_count": 0,
|
||||
"reblogs_count": 0,
|
||||
"favourites_count": 0,
|
||||
"favourited": false,
|
||||
"reblogged": false,
|
||||
"muted": false,
|
||||
"bookmarked": false,
|
||||
"pinned": false,
|
||||
"content": "",
|
||||
"reblog": null,
|
||||
"application": {
|
||||
"name": "Soapbox FE",
|
||||
"website": "https://soapbox.pub/"
|
||||
},
|
||||
"account": {
|
||||
"id": "107759994408336377",
|
||||
"username": "alex",
|
||||
"acct": "alex",
|
||||
"display_name": "Alex Gleason",
|
||||
"locked": false,
|
||||
"bot": false,
|
||||
"discoverable": null,
|
||||
"group": false,
|
||||
"created_at": "2022-02-08T00:00:00.000Z",
|
||||
"note": "<p>Launching Truth Social</p>",
|
||||
"url": "https://truthsocial.com/@alex",
|
||||
"avatar": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
|
||||
"avatar_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/avatars/107/759/994/408/336/377/original/119cb0dd1fa615b7.png",
|
||||
"header": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
|
||||
"header_static": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/accounts/headers/107/759/994/408/336/377/original/31f62b0453ccf554.png",
|
||||
"followers_count": 4713,
|
||||
"following_count": 43,
|
||||
"statuses_count": 7,
|
||||
"last_status_at": "2022-03-30",
|
||||
"verified": true,
|
||||
"location": "Texas",
|
||||
"website": "https://soapbox.pub/",
|
||||
"emojis": [],
|
||||
"fields": []
|
||||
},
|
||||
"media_attachments": [
|
||||
{
|
||||
"id": "108046243948255335",
|
||||
"type": "video",
|
||||
"url": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/media_attachments/files/108/046/243/948/255/335/original/3b17ce701c0d6f08.mp4",
|
||||
"preview_url": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/cache/preview_cards/images/000/543/912/original/e1fcf6ace01d9ded.jpg",
|
||||
"external_video_id": "vwfnq9",
|
||||
"remote_url": null,
|
||||
"preview_remote_url": null,
|
||||
"text_url": "https://truthsocial.com/media/SpbYvqKIT2VubC9FFn0",
|
||||
"meta": {
|
||||
"original": {
|
||||
"width": 988,
|
||||
"height": 556,
|
||||
"frame_rate": "60/1",
|
||||
"duration": 1.949025,
|
||||
"bitrate": 402396
|
||||
}
|
||||
},
|
||||
"description": null,
|
||||
"blurhash": null
|
||||
}
|
||||
],
|
||||
"mentions": [],
|
||||
"tags": [],
|
||||
"emojis": [],
|
||||
"card": {
|
||||
"url": "https://rumble.com/vz1trd-video-upload-for-108046244464677537.html?mref=ummtf&mc=3nvg0",
|
||||
"title": "Video upload for 108046244464677537",
|
||||
"description": "",
|
||||
"type": "video",
|
||||
"author_name": "hostid1",
|
||||
"author_url": "https://rumble.com/user/hostid1",
|
||||
"provider_name": "Rumble.com",
|
||||
"provider_url": "https://rumble.com/",
|
||||
"html": "<iframe src=\"https://rumble.com/embed/vwfnq9/\" width=\"988\" height=\"556\" frameborder=\"0\" title=\"Video upload for 108046244464677537\" allowfullscreen=\"\"></iframe>",
|
||||
"width": 988,
|
||||
"height": 556,
|
||||
"image": "https://static-assets.truthsocial.com/tmtg:prime-truth-social-assets/cache/preview_cards/images/000/543/912/original/e1fcf6ace01d9ded.jpg",
|
||||
"embed_url": "",
|
||||
"blurhash": "UQH1;m~8sks,%M~9?DRk-mRnR+xs9cWVj[bH"
|
||||
},
|
||||
"poll": null
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { InstanceRecord } from 'soapbox/normalizers';
|
||||
import rootReducer from 'soapbox/reducers';
|
||||
import { mockStore } from 'soapbox/test_helpers';
|
||||
|
||||
import { uploadCompose } from '../compose';
|
||||
|
||||
describe('uploadCompose()', () => {
|
||||
describe('with images', () => {
|
||||
let files, store;
|
||||
|
||||
beforeEach(() => {
|
||||
const instance = InstanceRecord({
|
||||
configuration: {
|
||||
statuses: {
|
||||
max_media_attachments: 4,
|
||||
},
|
||||
media_attachments: {
|
||||
image_size_limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const state = rootReducer(undefined, {})
|
||||
.set('me', '1234')
|
||||
.set('instance', instance);
|
||||
|
||||
store = mockStore(state);
|
||||
files = [{
|
||||
uri: 'image.png',
|
||||
name: 'Image',
|
||||
size: 15,
|
||||
type: 'image/png',
|
||||
}];
|
||||
});
|
||||
|
||||
it('creates an alert if exceeds max size', async() => {
|
||||
const mockIntl = {
|
||||
formatMessage: jest.fn().mockReturnValue('Image exceeds the current file size limit (10 Bytes)'),
|
||||
};
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||
{
|
||||
type: 'ALERT_SHOW',
|
||||
message: 'Image exceeds the current file size limit (10 Bytes)',
|
||||
actionLabel: undefined,
|
||||
actionLink: undefined,
|
||||
severity: 'error',
|
||||
},
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||
];
|
||||
|
||||
await store.dispatch(uploadCompose(files, mockIntl));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with videos', () => {
|
||||
let files, store;
|
||||
|
||||
beforeEach(() => {
|
||||
const instance = InstanceRecord({
|
||||
configuration: {
|
||||
statuses: {
|
||||
max_media_attachments: 4,
|
||||
},
|
||||
media_attachments: {
|
||||
video_size_limit: 10,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const state = rootReducer(undefined, {})
|
||||
.set('me', '1234')
|
||||
.set('instance', instance);
|
||||
|
||||
store = mockStore(state);
|
||||
files = [{
|
||||
uri: 'video.mp4',
|
||||
name: 'Video',
|
||||
size: 15,
|
||||
type: 'video/mp4',
|
||||
}];
|
||||
});
|
||||
|
||||
it('creates an alert if exceeds max size', async() => {
|
||||
const mockIntl = {
|
||||
formatMessage: jest.fn().mockReturnValue('Video exceeds the current file size limit (10 Bytes)'),
|
||||
};
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'COMPOSE_UPLOAD_REQUEST', skipLoading: true },
|
||||
{
|
||||
type: 'ALERT_SHOW',
|
||||
message: 'Video exceeds the current file size limit (10 Bytes)',
|
||||
actionLabel: undefined,
|
||||
actionLink: undefined,
|
||||
severity: 'error',
|
||||
},
|
||||
{ type: 'COMPOSE_UPLOAD_FAIL', error: true, skipLoading: true },
|
||||
];
|
||||
|
||||
await store.dispatch(uploadCompose(files, mockIntl));
|
||||
const actions = store.getActions();
|
||||
|
||||
expect(actions).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,60 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
|
||||
import IconWithCounter from 'soapbox/components/icon_with_counter';
|
||||
import { Icon, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IThumbNavigationLink {
|
||||
count?: number,
|
||||
src: string,
|
||||
text: string | React.ReactElement,
|
||||
to: string,
|
||||
exact?: boolean,
|
||||
paths?: Array<string>,
|
||||
}
|
||||
|
||||
const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, src, text, to, exact, paths }): JSX.Element => {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
const isActive = (): boolean => {
|
||||
if (paths) {
|
||||
return paths.some(path => pathname.startsWith(path));
|
||||
} else {
|
||||
return exact ? pathname === to : pathname.startsWith(to);
|
||||
}
|
||||
};
|
||||
|
||||
const active = isActive();
|
||||
|
||||
return (
|
||||
<NavLink to={to} exact={exact} className='thumb-navigation__link'>
|
||||
{count !== undefined ? (
|
||||
<IconWithCounter
|
||||
src={require('@tabler/icons/icons/messages.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600 dark:text-gray-300': !active,
|
||||
'text-primary-600': active,
|
||||
})}
|
||||
count={count}
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
src={src}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600 dark:text-gray-300': !active,
|
||||
'text-primary-600': active,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
{text}
|
||||
</Text>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThumbNavigationLink;
|
@ -1,152 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { NavLink, withRouter } from 'react-router-dom';
|
||||
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import IconWithCounter from 'soapbox/components/icon_with_counter';
|
||||
import { Icon, Text } from 'soapbox/components/ui';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const reportsCount = state.getIn(['admin', 'openReports']).count();
|
||||
const approvalCount = state.getIn(['admin', 'awaitingApproval']).count();
|
||||
const instance = state.get('instance');
|
||||
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
logo: getSoapboxConfig(state).get('logo'),
|
||||
notificationCount: state.getIn(['notifications', 'unread']),
|
||||
chatsCount: state.getIn(['chats', 'items']).reduce((acc, curr) => acc + Math.min(curr.get('unread', 0), 1), 0),
|
||||
dashboardCount: reportsCount + approvalCount,
|
||||
features: getFeatures(instance),
|
||||
};
|
||||
};
|
||||
|
||||
export default @withRouter
|
||||
@connect(mapStateToProps)
|
||||
class ThumbNavigation extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
logo: PropTypes.string,
|
||||
account: ImmutablePropTypes.record,
|
||||
dashboardCount: PropTypes.number,
|
||||
notificationCount: PropTypes.number,
|
||||
chatsCount: PropTypes.number,
|
||||
features: PropTypes.object.isRequired,
|
||||
location: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { account, notificationCount, chatsCount, location, features } = this.props;
|
||||
|
||||
return (
|
||||
<div className='thumb-navigation'>
|
||||
<NavLink to='/' exact className='thumb-navigation__link'>
|
||||
<Icon
|
||||
src={require('icons/feed.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': location.pathname !== '/',
|
||||
'text-primary-600': location.pathname === '/',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.home' defaultMessage='Home' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
|
||||
<NavLink to='/search' className='thumb-navigation__link'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/search.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': location.pathname !== '/search',
|
||||
'text-primary-600': location.pathname === '/search',
|
||||
})}
|
||||
/>
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.search' defaultMessage='Search' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
|
||||
{account && (
|
||||
<NavLink to='/notifications' className='thumb-navigation__link'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/bell.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': location.pathname !== '/notifications',
|
||||
'text-primary-600': location.pathname === '/notifications',
|
||||
})}
|
||||
count={notificationCount}
|
||||
/>
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.notifications' defaultMessage='Alerts' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
)}
|
||||
|
||||
{account && (
|
||||
features.chats ? (
|
||||
<NavLink to='/chats' className='thumb-navigation__link'>
|
||||
<IconWithCounter
|
||||
src={require('@tabler/icons/icons/messages.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': location.pathname !== '/chats',
|
||||
'text-primary-600': location.pathname === '/chats',
|
||||
})}
|
||||
count={chatsCount}
|
||||
/>
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.chats' defaultMessage='Chats' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
) : (
|
||||
<NavLink to='/messages' className='thumb-navigation__link'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/mail.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': !['/messages', '/conversations'].includes(location.pathname),
|
||||
'text-primary-600': ['/messages', '/conversations'].includes(location.pathname),
|
||||
})}
|
||||
/>
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* (account && isStaff(account)) && (
|
||||
<NavLink to='/admin' className='thumb-navigation__link'>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/dashboard.svg')}
|
||||
className={classNames({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': !location.pathname.startsWith('/admin'),
|
||||
'text-primary-600': location.pathname.startsWith('/admin'),
|
||||
})}
|
||||
count={dashboardCount}
|
||||
/>
|
||||
|
||||
<Text tag='span' size='xs'>
|
||||
<FormattedMessage id='navigation.dashboard' defaultMessage='Dashboard' />
|
||||
</Text>
|
||||
</NavLink>
|
||||
) */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ThumbNavigationLink from 'soapbox/components/thumb_navigation-link';
|
||||
import { useAppSelector, useOwnAccount } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
const ThumbNavigation: React.FC = (): JSX.Element => {
|
||||
const account = useOwnAccount();
|
||||
const notificationCount = useAppSelector((state) => state.notifications.unread);
|
||||
const chatsCount = useAppSelector((state) => state.chats.get('items').reduce((acc: number, curr: any) => acc + Math.min(curr.get('unread', 0), 1), 0));
|
||||
// const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||
const features = getFeatures(useAppSelector((state) => state.instance));
|
||||
|
||||
return (
|
||||
<div className='thumb-navigation'>
|
||||
<ThumbNavigationLink
|
||||
src={require('icons/feed.svg')}
|
||||
text={<FormattedMessage id='navigation.home' defaultMessage='Home' />}
|
||||
to='/'
|
||||
exact
|
||||
/>
|
||||
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/icons/search.svg')}
|
||||
text={<FormattedMessage id='navigation.search' defaultMessage='Search' />}
|
||||
to='/search'
|
||||
exact
|
||||
/>
|
||||
|
||||
{account && (
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/icons/bell.svg')}
|
||||
text={<FormattedMessage id='navigation.notifications' defaultMessage='Alerts' />}
|
||||
to='/notifications'
|
||||
exact
|
||||
count={notificationCount}
|
||||
/>
|
||||
)}
|
||||
|
||||
{account && (
|
||||
features.chats ? (
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/icons/messages.svg')}
|
||||
text={<FormattedMessage id='navigation.chats' defaultMessage='Chats' />}
|
||||
to='/chats'
|
||||
exact
|
||||
count={chatsCount}
|
||||
/>
|
||||
) : (
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/icons/mail.svg')}
|
||||
text={<FormattedMessage id='navigation.direct_messages' defaultMessage='Messages' />}
|
||||
to='/messages'
|
||||
paths={['/messages', '/conversations']}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* (account && isStaff(account)) && (
|
||||
<ThumbNavigationLink
|
||||
src={require('@tabler/icons/icons/dashboard.svg')}
|
||||
text={<FormattedMessage id='navigation.dashboard' defaultMessage='Dashboard' />}
|
||||
to='/admin'
|
||||
count={dashboardCount}
|
||||
/>
|
||||
) */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThumbNavigation;
|
@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
const UploadProgress = () => {
|
||||
const active = useAppSelector((state) => state.compose.get('is_uploading'));
|
||||
const progress = useAppSelector((state) => state.compose.get('progress'));
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/icons/cloud-upload.svg')}
|
||||
className='w-7 h-7 text-gray-500'
|
||||
/>
|
||||
|
||||
<Stack space={1}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />
|
||||
</Text>
|
||||
|
||||
<div className='w-full h-1.5 rounded-lg bg-gray-200 relative'>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
||||
{({ width }) =>
|
||||
(<div
|
||||
className='absolute left-0 top-0 h-1.5 bg-primary-600 rounded-lg'
|
||||
style={{ width: `${width}%` }}
|
||||
/>)
|
||||
}
|
||||
</Motion>
|
||||
</div>
|
||||
</Stack>
|
||||
</HStack>
|
||||
);
|
||||
};
|
||||
|
||||
export default UploadProgress;
|
@ -1,45 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import spring from 'react-motion/lib/spring';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
||||
import Motion from '../../ui/util/optional_motion';
|
||||
|
||||
export default class UploadProgress extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
progress: PropTypes.number,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active, progress } = this.props;
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='upload-progress'>
|
||||
<div className='upload-progress__icon'>
|
||||
<Icon id='upload' />
|
||||
</div>
|
||||
|
||||
<div className='upload-progress__message'>
|
||||
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />
|
||||
|
||||
<div className='upload-progress__backdrop'>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
||||
{({ width }) =>
|
||||
<div className='upload-progress__tracker' style={{ width: `${width}%` }} />
|
||||
}
|
||||
</Motion>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -1,105 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { logOut } from 'soapbox/actions/auth';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { getBaseURL } from 'soapbox/utils/accounts';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import { openModal } from '../../../actions/modals';
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const me = state.get('me');
|
||||
const account = state.getIn(['accounts', me]);
|
||||
const instance = state.get('instance');
|
||||
const features = getFeatures(instance);
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
account,
|
||||
soapboxConfig,
|
||||
profileDirectory: features.profileDirectory,
|
||||
federating: features.federating,
|
||||
showAliases: features.accountAliasesAPI,
|
||||
importAPI: features.importAPI,
|
||||
baseURL: getBaseURL(account),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onOpenHotkeys(e) {
|
||||
dispatch(openModal('HOTKEYS'));
|
||||
e.preventDefault();
|
||||
},
|
||||
onClickLogOut(e) {
|
||||
dispatch(logOut(intl));
|
||||
e.preventDefault();
|
||||
},
|
||||
});
|
||||
|
||||
const LinkFooter = ({ onOpenHotkeys, account, profileDirectory, federating, showAliases, importAPI, onClickLogOut, baseURL, soapboxConfig }) => (
|
||||
<div className='space-y-2'>
|
||||
<ul className='flex flex-wrap items-center divide-x-dot'>
|
||||
{account && <>
|
||||
{profileDirectory && <li><Link to='/directory' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></Link></li>}
|
||||
<li><Link to='/blocks' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></Link></li>
|
||||
<li><Link to='/mutes' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></Link></li>
|
||||
{/* <li><Link to='/filters' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></Link></li> */}
|
||||
{federating && <li><Link to='/domain_blocks' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></Link></li>}
|
||||
{/* <li><Link to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></Link></li> */}
|
||||
{/* isAdmin(account) && <li><a href='/pleroma/admin' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.admin_settings' defaultMessage='AdminFE' /></a></li> */}
|
||||
{/* isAdmin(account) && <li><Link to='/soapbox/config' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.soapbox_config' defaultMessage='Soapbox config' /></Link></li> */}
|
||||
{/* <li><Link to='/settings/export'><FormattedMessage id='navigation_bar.export_data' defaultMessage='Export data' /></Link></li> */}
|
||||
{/* <li>{importAPI ? (
|
||||
<Link to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></Link>
|
||||
) : (
|
||||
<a href={`${baseURL}/settings/import`}><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></a>
|
||||
)}</li> */}
|
||||
{(federating && showAliases) && <li><Link to='/settings/aliases' className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.account_aliases' defaultMessage='Account aliases' /></Link></li>}
|
||||
{/* <li><a href='#' onClick={onOpenHotkeys}><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></a></li> */}
|
||||
</>}
|
||||
{/* <li><Link to='/about'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></Link></li> */}
|
||||
{account && <li><Link to='/auth/sign_out' onClick={onClickLogOut} className='text-gray-400 hover:text-gray-500 hover:underline'><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></Link></li>}
|
||||
</ul>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
{soapboxConfig.get('linkFooterMessage') ? (
|
||||
<span
|
||||
className='inline-block align-middle'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(soapboxConfig.get('linkFooterMessage')) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).'
|
||||
values={{
|
||||
code_name: sourceCode.displayName,
|
||||
code_link: <a href={sourceCode.url} rel='noopener' target='_blank'>{sourceCode.repository}</a>,
|
||||
code_version: sourceCode.version,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
LinkFooter.propTypes = {
|
||||
account: ImmutablePropTypes.record,
|
||||
soapboxConfig: ImmutablePropTypes.map,
|
||||
profileDirectory: PropTypes.bool,
|
||||
federating: PropTypes.bool,
|
||||
showAliases: PropTypes.bool,
|
||||
importAPI: PropTypes.bool,
|
||||
onOpenHotkeys: PropTypes.func.isRequired,
|
||||
onClickLogOut: PropTypes.func.isRequired,
|
||||
baseURL: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(LinkFooter));
|
@ -0,0 +1,88 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { logOut } from 'soapbox/actions/auth';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import { useSoapboxConfig, useOwnAccount, useFeatures } from 'soapbox/hooks';
|
||||
import sourceCode from 'soapbox/utils/code';
|
||||
|
||||
interface IFooterLink {
|
||||
to: string,
|
||||
className?: string,
|
||||
onClick?: React.EventHandler<React.MouseEvent>,
|
||||
}
|
||||
|
||||
const FooterLink: React.FC<IFooterLink> = ({ children, className, ...rest }): JSX.Element => {
|
||||
return (
|
||||
<Link className={classNames('text-gray-400 hover:text-gray-500 hover:underline', className)} {...rest}>{children}</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const LinkFooter: React.FC = (): JSX.Element => {
|
||||
const account = useOwnAccount();
|
||||
const features = useFeatures();
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onClickLogOut: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
dispatch(logOut(intl));
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='space-y-2'>
|
||||
<div className='flex flex-wrap items-center divide-x-dot'>
|
||||
{account && <>
|
||||
{features.profileDirectory && (
|
||||
<FooterLink to='/directory'><FormattedMessage id='navigation_bar.profile_directory' defaultMessage='Profile directory' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/blocks'><FormattedMessage id='navigation_bar.blocks' defaultMessage='Blocks' /></FooterLink>
|
||||
<FooterLink to='/mutes'><FormattedMessage id='navigation_bar.mutes' defaultMessage='Mutes' /></FooterLink>
|
||||
{features.filters && (
|
||||
<FooterLink to='/filters'><FormattedMessage id='navigation_bar.filters' defaultMessage='Filters' /></FooterLink>
|
||||
)}
|
||||
{features.federating && (
|
||||
<FooterLink to='/domain_blocks'><FormattedMessage id='navigation_bar.domain_blocks' defaultMessage='Domain blocks' /></FooterLink>
|
||||
)}
|
||||
{account.locked && (
|
||||
<FooterLink to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></FooterLink>
|
||||
)}
|
||||
{features.importAPI && (
|
||||
<FooterLink to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></FooterLink>
|
||||
)}
|
||||
{(features.federating && features.accountMoving) && (
|
||||
<FooterLink to='/settings/migration'><FormattedMessage id='navigation_bar.account_migration' defaultMessage='Move account' /></FooterLink>
|
||||
)}
|
||||
<FooterLink to='/auth/sign_out' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink>
|
||||
</>}
|
||||
</div>
|
||||
|
||||
<Text theme='muted' size='sm'>
|
||||
{soapboxConfig.linkFooterMessage ? (
|
||||
<span
|
||||
className='inline-block align-middle'
|
||||
dangerouslySetInnerHTML={{ __html: emojify(soapboxConfig.linkFooterMessage) }}
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='getting_started.open_source_notice'
|
||||
defaultMessage='{code_name} is open source software. You can contribute or report issues at {code_link} (v{code_version}).'
|
||||
values={{
|
||||
code_name: sourceCode.displayName,
|
||||
code_link: <a href={sourceCode.url} rel='noopener' target='_blank'>{sourceCode.repository}</a>,
|
||||
code_version: sourceCode.version,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LinkFooter;
|
@ -0,0 +1,9 @@
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { getFeatures } from 'soapbox/utils/features';
|
||||
|
||||
import type { Features } from 'soapbox/utils/features';
|
||||
|
||||
/** Get features for the current instance */
|
||||
export const useFeatures = (): Features => {
|
||||
return useAppSelector((state) => getFeatures(state.instance));
|
||||
};
|
@ -1,21 +0,0 @@
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import startCase from 'lodash/startCase';
|
||||
|
||||
const toSentence = (arr) => arr
|
||||
.reduce(
|
||||
(prev, curr, i) => prev + curr + (i === arr.length - 2 ? ' and ' : ', '),
|
||||
'',
|
||||
)
|
||||
.slice(0, -2);
|
||||
|
||||
const buildErrorMessage = (errors) => {
|
||||
const individualErrors = Object.keys(errors).map(
|
||||
(attribute) => `${startCase(camelCase(attribute))} ${toSentence(
|
||||
errors[attribute],
|
||||
)}`,
|
||||
);
|
||||
|
||||
return toSentence(individualErrors);
|
||||
};
|
||||
|
||||
export { buildErrorMessage };
|
@ -0,0 +1,203 @@
|
||||
import camelCase from 'lodash/camelCase';
|
||||
import startCase from 'lodash/startCase';
|
||||
|
||||
const toSentence = (arr: string[]) => arr
|
||||
.reduce(
|
||||
(prev, curr, i) => prev + curr + (i === arr.length - 2 ? ' and ' : ', '),
|
||||
'',
|
||||
)
|
||||
.slice(0, -2);
|
||||
|
||||
type Errors = {
|
||||
[key: string]: string[]
|
||||
}
|
||||
|
||||
const buildErrorMessage = (errors: Errors) => {
|
||||
const individualErrors = Object.keys(errors).map(
|
||||
(attribute) => `${startCase(camelCase(attribute))} ${toSentence(
|
||||
errors[attribute],
|
||||
)}`,
|
||||
);
|
||||
|
||||
return toSentence(individualErrors);
|
||||
};
|
||||
|
||||
const httpErrorMessages: { code: number, name: string, description: string }[] = [
|
||||
{
|
||||
code: 100,
|
||||
name: 'Continue',
|
||||
description: 'The server has received the request headers, and the client should proceed to send the request body',
|
||||
},
|
||||
{
|
||||
code: 101,
|
||||
name: 'Switching Protocols',
|
||||
description: 'The requester has asked the server to switch protocols',
|
||||
},
|
||||
{
|
||||
code: 103,
|
||||
name: 'Checkpoint',
|
||||
description: 'Used in the resumable requests proposal to resume aborted PUT or POST requests',
|
||||
},
|
||||
{
|
||||
code: 200,
|
||||
name: 'OK',
|
||||
description: 'The request is OK (this is the standard response for successful HTTP requests)',
|
||||
},
|
||||
{
|
||||
code: 201,
|
||||
name: 'Created',
|
||||
description: 'The request has been fulfilled',
|
||||
},
|
||||
{
|
||||
code: 202,
|
||||
name: 'Accepted',
|
||||
description: 'The request has been accepted for processing',
|
||||
},
|
||||
{
|
||||
code: 203,
|
||||
name: 'Non-Authoritative Information',
|
||||
description: 'The request has been successfully processed',
|
||||
},
|
||||
{
|
||||
code: 204,
|
||||
name: 'No Content',
|
||||
description: 'The request has been successfully processed',
|
||||
},
|
||||
{
|
||||
code: 205,
|
||||
name: 'Reset Content',
|
||||
description: 'The request has been successfully processed',
|
||||
},
|
||||
{
|
||||
code: 206,
|
||||
name: 'Partial Content',
|
||||
description: 'The server is delivering only part of the resource due to a range header sent by the client',
|
||||
},
|
||||
{
|
||||
code: 400,
|
||||
name: 'Bad Request',
|
||||
description: 'The request cannot be fulfilled due to bad syntax',
|
||||
},
|
||||
{
|
||||
code: 401,
|
||||
name: 'Unauthorized',
|
||||
description: 'The request was a legal request',
|
||||
},
|
||||
{
|
||||
code: 402,
|
||||
name: 'Payment Required',
|
||||
description: 'Reserved for future use',
|
||||
},
|
||||
{
|
||||
code: 403,
|
||||
name: 'Forbidden',
|
||||
description: 'The request was a legal request',
|
||||
},
|
||||
{
|
||||
code: 404,
|
||||
name: 'Not Found',
|
||||
description: 'The requested page could not be found but may be available again in the future',
|
||||
},
|
||||
{
|
||||
code: 405,
|
||||
name: 'Method Not Allowed',
|
||||
description: 'A request was made of a page using a request method not supported by that page',
|
||||
},
|
||||
{
|
||||
code: 406,
|
||||
name: 'Not Acceptable',
|
||||
description: 'The server can only generate a response that is not accepted by the client',
|
||||
},
|
||||
{
|
||||
code: 407,
|
||||
name: 'Proxy Authentication Required',
|
||||
description: 'The client must first authenticate itself with the proxy',
|
||||
},
|
||||
{
|
||||
code: 408,
|
||||
name: 'Request',
|
||||
description: ' Timeout\tThe server timed out waiting for the request',
|
||||
},
|
||||
{
|
||||
code: 409,
|
||||
name: 'Conflict',
|
||||
description: 'The request could not be completed because of a conflict in the request',
|
||||
},
|
||||
{
|
||||
code: 410,
|
||||
name: 'Gone',
|
||||
description: 'The requested page is no longer available',
|
||||
},
|
||||
{
|
||||
code: 411,
|
||||
name: 'Length Required',
|
||||
description: 'The "Content-Length" is not defined. The server will not accept the request without it',
|
||||
},
|
||||
{
|
||||
code: 412,
|
||||
name: 'Precondition',
|
||||
description: ' Failed. The precondition given in the request evaluated to false by the server',
|
||||
},
|
||||
{
|
||||
code: 413,
|
||||
name: 'Request Entity Too Large',
|
||||
description: 'The server will not accept the request',
|
||||
},
|
||||
{
|
||||
code: 414,
|
||||
name: 'Request-URI Too Long',
|
||||
description: 'The server will not accept the request',
|
||||
},
|
||||
{
|
||||
code: 415,
|
||||
name: 'Unsupported Media Type',
|
||||
description: 'The server will not accept the request',
|
||||
},
|
||||
{
|
||||
code: 416,
|
||||
name: 'Requested Range Not Satisfiable',
|
||||
description: 'The client has asked for a portion of the file',
|
||||
},
|
||||
{
|
||||
code: 417,
|
||||
name: 'Expectation Failed',
|
||||
description: 'The server cannot meet the requirements of the Expect request-header field',
|
||||
},
|
||||
{
|
||||
code: 500,
|
||||
name: 'Internal Server Error',
|
||||
description: 'An unexpected error occurred',
|
||||
},
|
||||
{
|
||||
code: 501,
|
||||
name: 'Not Implemented',
|
||||
description: 'The server either does not recognize the request method',
|
||||
},
|
||||
{
|
||||
code: 502,
|
||||
name: 'Bad Gateway',
|
||||
description: 'The server was acting as a gateway or proxy and received an invalid response from the upstream server',
|
||||
},
|
||||
{
|
||||
code: 503,
|
||||
name: 'Service Unavailable',
|
||||
description: 'The server is currently unavailable (overloaded or down)',
|
||||
},
|
||||
{
|
||||
code: 504,
|
||||
name: 'Gateway Timeout',
|
||||
description: 'The server was acting as a gateway or proxy and did not receive a timely response from the upstream server',
|
||||
},
|
||||
{
|
||||
code: 505,
|
||||
name: 'HTTP Version Not Supported',
|
||||
description: 'The server does not support the HTTP protocol version used in the request',
|
||||
},
|
||||
{
|
||||
code: 511,
|
||||
name: 'Network Authentication Required',
|
||||
description: 'The client needs to auth',
|
||||
},
|
||||
];
|
||||
|
||||
export { buildErrorMessage, httpErrorMessages };
|
@ -1,10 +0,0 @@
|
||||
export const truncateFilename = (url, maxLength) => {
|
||||
const filename = url.split('/').pop();
|
||||
|
||||
if (filename.length <= maxLength) return filename;
|
||||
|
||||
return [
|
||||
filename.substr(0, maxLength/2),
|
||||
filename.substr(filename.length - maxLength/2),
|
||||
].join('…');
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
const truncateFilename = (url: string, maxLength: number) => {
|
||||
const filename = url.split('/').pop();
|
||||
|
||||
if (!filename) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
if (filename.length <= maxLength) return filename;
|
||||
|
||||
return [
|
||||
filename.substr(0, maxLength/2),
|
||||
filename.substr(filename.length - maxLength/2),
|
||||
].join('…');
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number, decimals: number = 2) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
export { formatBytes, truncateFilename };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue