Merge branch 'glitch-soc' into develop
commit
8f8cb0fad3
@ -1,26 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { length } from 'stringz';
|
||||
|
||||
export default class CharacterCounter extends PureComponent {
|
||||
export const CharacterCounter = ({ text, max }) => {
|
||||
const diff = max - length(text);
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
checkRemainingText (diff) {
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const diff = this.props.max - length(this.props.text);
|
||||
return this.checkRemainingText(diff);
|
||||
}
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
};
|
||||
|
||||
}
|
||||
CharacterCounter.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
@ -0,0 +1,66 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import CodeIcon from '@/material-icons/400-24px/code.svg?react';
|
||||
import DescriptionIcon from '@/material-icons/400-24px/description.svg?react';
|
||||
import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react';
|
||||
import { changeComposeContentType } from 'flavours/glitch/actions/compose';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
import { DropdownIconButton } from './dropdown_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
change_content_type: { id: 'compose.content-type.change', defaultMessage: 'Change advanced formatting options' },
|
||||
plain_text_label: { id: 'compose.content-type.plain', defaultMessage: 'Plain text' },
|
||||
plain_text_meta: { id: 'compose.content-type.plain_meta', defaultMessage: 'Write with no advanced formatting' },
|
||||
markdown_label: { id: 'compose.content-type.markdown', defaultMessage: 'Markdown' },
|
||||
markdown_meta: { id: 'compose.content-type.markdown_meta', defaultMessage: 'Format your posts using Markdown' },
|
||||
html_label: { id: 'compose.content-type.html', defaultMessage: 'HTML' },
|
||||
html_meta: { id: 'compose.content-type.html_meta', defaultMessage: 'Format your posts using HTML' },
|
||||
});
|
||||
|
||||
export const ContentTypeButton = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const showButton = useAppSelector((state) => state.getIn(['local_settings', 'show_content_type_choice']));
|
||||
const contentType = useAppSelector((state) => state.getIn(['compose', 'content_type']));
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChange = useCallback((value) => {
|
||||
dispatch(changeComposeContentType(value));
|
||||
}, [dispatch]);
|
||||
|
||||
if (!showButton) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ icon: 'file-text', iconComponent: DescriptionIcon, value: 'text/plain', text: intl.formatMessage(messages.plain_text_label), meta: intl.formatMessage(messages.plain_text_meta) },
|
||||
{ icon: 'arrow-circle-down', iconComponent: MarkdownIcon, value: 'text/markdown', text: intl.formatMessage(messages.markdown_label), meta: intl.formatMessage(messages.markdown_meta) },
|
||||
{ icon: 'code', iconComponent: CodeIcon, value: 'text/html', text: intl.formatMessage(messages.html_label), meta: intl.formatMessage(messages.html_meta) },
|
||||
];
|
||||
|
||||
const icon = {
|
||||
'text/plain': 'file-text',
|
||||
'text/markdown': 'arrow-circle-down',
|
||||
'text/html': 'code',
|
||||
}[contentType];
|
||||
|
||||
const iconComponent = {
|
||||
'text/plain': DescriptionIcon,
|
||||
'text/markdown': MarkdownIcon,
|
||||
'text/html': CodeIcon,
|
||||
}[contentType];
|
||||
|
||||
return (
|
||||
<DropdownIconButton
|
||||
icon={icon}
|
||||
iconComponent={iconComponent}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
title={intl.formatMessage(messages.change_content_type)}
|
||||
value={contentType}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,243 +0,0 @@
|
||||
// Package imports.
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
// Components.
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
import DropdownMenu from './dropdown_menu';
|
||||
|
||||
// The component.
|
||||
export default class ComposerOptionsDropdown extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
isUserTouching: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
items: PropTypes.arrayOf(PropTypes.shape({
|
||||
icon: PropTypes.string,
|
||||
iconComponent: PropTypes.func,
|
||||
meta: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
text: PropTypes.string,
|
||||
})).isRequired,
|
||||
onModalOpen: PropTypes.func,
|
||||
onModalClose: PropTypes.func,
|
||||
title: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
container: PropTypes.func,
|
||||
renderItemContents: PropTypes.func,
|
||||
closeOnChange: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
closeOnChange: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
openedViaKeyboard: undefined,
|
||||
placement: 'bottom',
|
||||
};
|
||||
|
||||
// Toggles opening and closing the dropdown.
|
||||
handleToggle = ({ type }) => {
|
||||
const { onModalOpen } = this.props;
|
||||
const { open } = this.state;
|
||||
|
||||
if (this.props.isUserTouching && this.props.isUserTouching()) {
|
||||
if (open) {
|
||||
this.props.onModalClose();
|
||||
} else {
|
||||
const modal = this.handleMakeModal();
|
||||
if (modal && onModalOpen) {
|
||||
onModalOpen(modal);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: !open, openedViaKeyboard: type !== 'click' });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (e) => {
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
this.handleClose();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleMouseDown = () => {
|
||||
if (!this.state.open) {
|
||||
this.activeElement = document.activeElement;
|
||||
}
|
||||
};
|
||||
|
||||
handleButtonKeyDown = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleMouseDown();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyPress = (e) => {
|
||||
switch(e.key) {
|
||||
case ' ':
|
||||
case 'Enter':
|
||||
this.handleToggle(e);
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
if (this.state.open && this.activeElement) {
|
||||
this.activeElement.focus({ preventScroll: true });
|
||||
}
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
handleItemClick = (e) => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onModalClose,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
|
||||
const i = Number(e.currentTarget.getAttribute('data-index'));
|
||||
|
||||
const { name } = items[i];
|
||||
|
||||
e.preventDefault(); // Prevents focus from changing
|
||||
if (closeOnChange) onModalClose();
|
||||
onChange(name);
|
||||
};
|
||||
|
||||
// Creates an action modal object.
|
||||
handleMakeModal = () => {
|
||||
const {
|
||||
items,
|
||||
onChange,
|
||||
onModalOpen,
|
||||
onModalClose,
|
||||
value,
|
||||
} = this.props;
|
||||
|
||||
// Required props.
|
||||
if (!(onChange && onModalOpen && onModalClose && items)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// The object.
|
||||
return {
|
||||
renderItemContents: this.props.renderItemContents,
|
||||
onClick: this.handleItemClick,
|
||||
actions: items.map(
|
||||
({
|
||||
name,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
active: value && name === value,
|
||||
name,
|
||||
}),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
};
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
};
|
||||
|
||||
handleOverlayEnter = (state) => {
|
||||
this.setState({ placement: state.placement });
|
||||
};
|
||||
|
||||
// Rendering.
|
||||
render () {
|
||||
const {
|
||||
disabled,
|
||||
title,
|
||||
icon,
|
||||
iconComponent,
|
||||
items,
|
||||
onChange,
|
||||
value,
|
||||
container,
|
||||
renderItemContents,
|
||||
closeOnChange,
|
||||
} = this.props;
|
||||
const { open, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('privacy-dropdown', placement, { active: open })}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
ref={this.setTargetRef}
|
||||
>
|
||||
<IconButton
|
||||
active={open}
|
||||
className='privacy-dropdown__value-icon'
|
||||
disabled={disabled}
|
||||
icon={icon}
|
||||
iconComponent={iconComponent}
|
||||
inverted
|
||||
onClick={this.handleToggle}
|
||||
onMouseDown={this.handleMouseDown}
|
||||
onKeyDown={this.handleButtonKeyDown}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
size={18}
|
||||
style={{
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
}}
|
||||
title={title}
|
||||
/>
|
||||
|
||||
<Overlay
|
||||
containerPadding={20}
|
||||
placement={placement}
|
||||
show={open}
|
||||
flip
|
||||
target={this.findTarget}
|
||||
container={container}
|
||||
popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}
|
||||
>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<DropdownMenu
|
||||
items={items}
|
||||
renderItemContents={renderItemContents}
|
||||
onChange={onChange}
|
||||
onClose={this.handleClose}
|
||||
value={value}
|
||||
openedViaKeyboard={this.state.openedViaKeyboard}
|
||||
closeOnChange={closeOnChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback, useState, useRef } from 'react';
|
||||
|
||||
import Overlay from 'react-overlays/Overlay';
|
||||
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
import DropdownMenu from './dropdown_menu';
|
||||
|
||||
export const DropdownIconButton = ({ value, disabled, icon, onChange, iconComponent, title, options }) => {
|
||||
const containerRef = useRef(null);
|
||||
|
||||
const [activeElement, setActiveElement] = useState(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [placement, setPlacement] = useState('bottom');
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
if (open && activeElement) {
|
||||
activeElement.focus({ preventScroll: true });
|
||||
setActiveElement(null);
|
||||
}
|
||||
|
||||
setOpen(!open);
|
||||
}, [open, setOpen, activeElement, setActiveElement]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (open && activeElement) {
|
||||
activeElement.focus({ preventScroll: true });
|
||||
setActiveElement(null);
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
}, [open, setOpen, activeElement, setActiveElement]);
|
||||
|
||||
const handleOverlayEnter = useCallback((state) => {
|
||||
setPlacement(state.placement);
|
||||
}, [setPlacement]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
icon={icon}
|
||||
onClick={handleToggle}
|
||||
iconComponent={iconComponent}
|
||||
title={title}
|
||||
active={open}
|
||||
size={18}
|
||||
inverted
|
||||
/>
|
||||
|
||||
<Overlay show={open} offset={[5, 5]} placement={placement} flip target={containerRef} popperConfig={{ strategy: 'fixed', onFirstUpdate: handleOverlayEnter }}>
|
||||
{({ props, placement }) => (
|
||||
<div {...props}>
|
||||
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
|
||||
<DropdownMenu
|
||||
items={options}
|
||||
value={value}
|
||||
onClose={handleClose}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownIconButton.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
icon: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
iconComponent: PropTypes.func.isRequired,
|
||||
options: PropTypes.array.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
};
|
@ -0,0 +1,61 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
export const EditIndicator = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(state => state.getIn(['compose', 'id']));
|
||||
const status = useSelector(state => state.getIn(['statuses', id]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='edit-indicator'>
|
||||
<div className='edit-indicator__header'>
|
||||
<div className='edit-indicator__display-name'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`}>@{account.get('acct')}</Permalink>
|
||||
·
|
||||
<Permalink href={status.get('url')} to={`/@${account.get('acct')}/${status.get('id')}`}><RelativeTimestamp timestamp={status.get('created_at')} /></Permalink>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='edit-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,47 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
|
||||
import ShareOffIcon from '@/material-icons/400-24px/share_off.svg?react';
|
||||
import { changeComposeAdvancedOption } from 'flavours/glitch/actions/compose';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
import { DropdownIconButton } from './dropdown_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
change_federation_settings: { id: 'compose.change_federation', defaultMessage: 'Change federation settings' },
|
||||
local_only_label: { id: 'federation.local_only.short', defaultMessage: 'Local-only' },
|
||||
local_only_meta: { id: 'federation.local_only.long', defaultMessage: 'Prevent this post from reaching other servers' },
|
||||
federated_label: { id: 'federation.federated.short', defaultMessage: 'Federated' },
|
||||
federated_meta: { id: 'federation.federated.long', defaultMessage: 'Allow this post to reach other servers' },
|
||||
});
|
||||
|
||||
export const FederationButton = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const isEditing = useAppSelector((state) => state.getIn(['compose', 'id']) !== null);
|
||||
const do_not_federate = useAppSelector((state) => state.getIn(['compose', 'advanced_options', 'do_not_federate']));
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleChange = useCallback((value) => {
|
||||
dispatch(changeComposeAdvancedOption('do_not_federate', value === 'local-only'));
|
||||
}, [dispatch]);
|
||||
|
||||
const options = [
|
||||
{ icon: 'link', iconComponent: ShareIcon, value: 'federated', text: intl.formatMessage(messages.federated_label), meta: intl.formatMessage(messages.federated_meta) },
|
||||
{ icon: 'link-slash', iconComponent: ShareOffIcon, value: 'local-only', text: intl.formatMessage(messages.local_only_label), meta: intl.formatMessage(messages.local_only_meta) },
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownIconButton
|
||||
disabled={isEditing}
|
||||
icon={do_not_federate ? 'link-slash' : 'link'}
|
||||
iconComponent={do_not_federate ? ShareOffIcon : ShareIcon}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
title={intl.formatMessage(messages.change_federation_settings)}
|
||||
value={do_not_federate ? 'local-only' : 'federated'}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,149 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react';
|
||||
import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react';
|
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react';
|
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { signOutLink } from 'flavours/glitch/utils/backend_links';
|
||||
import { conditionalRender } from 'flavours/glitch/utils/react_helpers';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
community: {
|
||||
defaultMessage: 'Local timeline',
|
||||
id: 'navigation_bar.community_timeline',
|
||||
},
|
||||
home_timeline: {
|
||||
defaultMessage: 'Home',
|
||||
id: 'tabs_bar.home',
|
||||
},
|
||||
logout: {
|
||||
defaultMessage: 'Logout',
|
||||
id: 'navigation_bar.logout',
|
||||
},
|
||||
notifications: {
|
||||
defaultMessage: 'Notifications',
|
||||
id: 'tabs_bar.notifications',
|
||||
},
|
||||
public: {
|
||||
defaultMessage: 'Federated timeline',
|
||||
id: 'navigation_bar.public_timeline',
|
||||
},
|
||||
settings: {
|
||||
defaultMessage: 'App settings',
|
||||
id: 'navigation_bar.app_settings',
|
||||
},
|
||||
start: {
|
||||
defaultMessage: 'Getting started',
|
||||
id: 'getting_started.heading',
|
||||
},
|
||||
});
|
||||
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
columns: ImmutablePropTypes.list,
|
||||
unreadNotifications: PropTypes.number,
|
||||
showNotificationsBadge: PropTypes.bool,
|
||||
intl: PropTypes.object,
|
||||
onSettingsClick: PropTypes.func,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleLogoutClick = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
this.props.onLogout();
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props;
|
||||
|
||||
// Only renders the component if the column isn't being shown.
|
||||
const renderForColumn = conditionalRender.bind(null,
|
||||
columnId => !columns || !columns.some(
|
||||
column => column.get('id') === columnId,
|
||||
),
|
||||
);
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<nav className='drawer__header'>
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.start)}
|
||||
title={intl.formatMessage(messages.start)}
|
||||
to='/getting-started'
|
||||
className='drawer__tab'
|
||||
><Icon id='bars' icon={MenuIcon} /></Link>
|
||||
{renderForColumn('HOME', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.home_timeline)}
|
||||
title={intl.formatMessage(messages.home_timeline)}
|
||||
to='/home'
|
||||
className='drawer__tab'
|
||||
><Icon id='home' icon={HomeIcon} /></Link>
|
||||
))}
|
||||
{renderForColumn('NOTIFICATIONS', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.notifications)}
|
||||
title={intl.formatMessage(messages.notifications)}
|
||||
to='/notifications'
|
||||
className='drawer__tab'
|
||||
>
|
||||
<span className='icon-badge-wrapper'>
|
||||
<Icon id='bell' icon={NotificationsIcon} />
|
||||
{ showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
{renderForColumn('COMMUNITY', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.community)}
|
||||
title={intl.formatMessage(messages.community)}
|
||||
to='/public/local'
|
||||
className='drawer__tab'
|
||||
><Icon id='users' icon={PeopleIcon} /></Link>
|
||||
))}
|
||||
{renderForColumn('PUBLIC', (
|
||||
<Link
|
||||
aria-label={intl.formatMessage(messages.public)}
|
||||
title={intl.formatMessage(messages.public)}
|
||||
to='/public'
|
||||
className='drawer__tab'
|
||||
><Icon id='globe' icon={PublicIcon} /></Link>
|
||||
))}
|
||||
<a
|
||||
aria-label={intl.formatMessage(messages.settings)}
|
||||
onClick={onSettingsClick}
|
||||
href='/settings/preferences'
|
||||
title={intl.formatMessage(messages.settings)}
|
||||
className='drawer__tab'
|
||||
><Icon id='cogs' icon={ManufacturingIcon} /></a>
|
||||
<a
|
||||
aria-label={intl.formatMessage(messages.logout)}
|
||||
onClick={this.handleLogoutClick}
|
||||
href={signOutLink}
|
||||
title={intl.formatMessage(messages.logout)}
|
||||
className='drawer__tab'
|
||||
><Icon id='sign-out' icon={LogoutIcon} /></a>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(Header);
|
@ -1,54 +1,36 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { profileLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
|
||||
import ActionBar from './action_bar';
|
||||
|
||||
export default class NavigationBar extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
render () {
|
||||
const username = this.props.account.get('acct');
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Permalink className='avatar' href={this.props.account.get('url')} to={`/@${username}`}>
|
||||
<span style={{ display: 'none' }}>{username}</span>
|
||||
<Avatar account={this.props.account} size={46} />
|
||||
</Permalink>
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<span>
|
||||
<Permalink className='acct' href={this.props.account.get('url')} to={`/@${username}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
||||
</Permalink>
|
||||
</span>
|
||||
|
||||
{ profileLink !== undefined && (
|
||||
<a
|
||||
href={profileLink}
|
||||
className='navigation-bar__profile-edit'
|
||||
><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import { cancelReplyCompose } from 'flavours/glitch/actions/compose';
|
||||
import Account from 'flavours/glitch/components/account';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { me } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { ActionBar } from './action_bar';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
export const NavigationBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const account = useSelector(state => state.getIn(['accounts', me]));
|
||||
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Account account={account} minimal />
|
||||
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,330 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Toggle from 'react-toggle';
|
||||
|
||||
import AttachFileIcon from '@/material-icons/400-24px/attach_file.svg?react';
|
||||
import BrushIcon from '@/material-icons/400-24px/brush.svg?react';
|
||||
import CodeIcon from '@/material-icons/400-24px/code.svg?react';
|
||||
import DescriptionIcon from '@/material-icons/400-24px/description.svg?react';
|
||||
import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react';
|
||||
import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react';
|
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { pollLimits } from 'flavours/glitch/initial_state';
|
||||
|
||||
|
||||
import DropdownContainer from '../containers/dropdown_container';
|
||||
import LanguageDropdown from '../containers/language_dropdown_container';
|
||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||
|
||||
import TextIconButton from './text_icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
advanced_options_icon_title: {
|
||||
defaultMessage: 'Advanced options',
|
||||
id: 'advanced_options.icon_title',
|
||||
},
|
||||
attach: {
|
||||
defaultMessage: 'Attach...',
|
||||
id: 'compose.attach',
|
||||
},
|
||||
content_type: {
|
||||
defaultMessage: 'Content type',
|
||||
id: 'content-type.change',
|
||||
},
|
||||
doodle: {
|
||||
defaultMessage: 'Draw something',
|
||||
id: 'compose.attach.doodle',
|
||||
},
|
||||
html: {
|
||||
defaultMessage: 'HTML',
|
||||
id: 'compose.content-type.html',
|
||||
},
|
||||
local_only_long: {
|
||||
defaultMessage: 'Do not post to other instances',
|
||||
id: 'advanced_options.local-only.long',
|
||||
},
|
||||
local_only_short: {
|
||||
defaultMessage: 'Local-only',
|
||||
id: 'advanced_options.local-only.short',
|
||||
},
|
||||
markdown: {
|
||||
defaultMessage: 'Markdown',
|
||||
id: 'compose.content-type.markdown',
|
||||
},
|
||||
plain: {
|
||||
defaultMessage: 'Plain text',
|
||||
id: 'compose.content-type.plain',
|
||||
},
|
||||
spoiler: {
|
||||
defaultMessage: 'Hide text behind warning',
|
||||
id: 'compose_form.spoiler',
|
||||
},
|
||||
threaded_mode_long: {
|
||||
defaultMessage: 'Automatically opens a reply on posting',
|
||||
id: 'advanced_options.threaded_mode.long',
|
||||
},
|
||||
threaded_mode_short: {
|
||||
defaultMessage: 'Threaded mode',
|
||||
id: 'advanced_options.threaded_mode.short',
|
||||
},
|
||||
upload: {
|
||||
defaultMessage: 'Upload a file',
|
||||
id: 'compose.attach.upload',
|
||||
},
|
||||
add_poll: {
|
||||
defaultMessage: 'Add a poll',
|
||||
id: 'poll_button.add_poll',
|
||||
},
|
||||
remove_poll: {
|
||||
defaultMessage: 'Remove poll',
|
||||
id: 'poll_button.remove_poll',
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { name }) => ({
|
||||
checked: state.getIn(['compose', 'advanced_options', name]),
|
||||
});
|
||||
|
||||
class ToggleOptionImpl extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
checked: PropTypes.bool,
|
||||
onChangeAdvancedOption: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleChange = () => {
|
||||
this.props.onChangeAdvancedOption(this.props.name);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { meta, text, checked } = this.props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='privacy-dropdown__option__icon'>
|
||||
<Toggle checked={checked} onChange={this.handleChange} />
|
||||
</div>
|
||||
|
||||
<div className='privacy-dropdown__option__content'>
|
||||
<strong>{text}</strong>
|
||||
{meta}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const ToggleOption = connect(mapStateToProps)(ToggleOptionImpl);
|
||||
|
||||
class ComposerOptions extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
acceptContentTypes: PropTypes.string,
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
disabled: PropTypes.bool,
|
||||
allowMedia: PropTypes.bool,
|
||||
allowPoll: PropTypes.bool,
|
||||
hasPoll: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onChangeAdvancedOption: PropTypes.func.isRequired,
|
||||
onChangeContentType: PropTypes.func.isRequired,
|
||||
onTogglePoll: PropTypes.func.isRequired,
|
||||
onDoodleOpen: PropTypes.func.isRequired,
|
||||
onToggleSpoiler: PropTypes.func,
|
||||
onUpload: PropTypes.func.isRequired,
|
||||
contentType: PropTypes.string,
|
||||
resetFileKey: PropTypes.number,
|
||||
spoiler: PropTypes.bool,
|
||||
showContentTypeChoice: PropTypes.bool,
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleChangeFiles = ({ target: { files } }) => {
|
||||
const { onUpload } = this.props;
|
||||
if (files.length) {
|
||||
onUpload(files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClickAttach = (name) => {
|
||||
const { fileElement } = this;
|
||||
const { onDoodleOpen } = this.props;
|
||||
|
||||
switch (name) {
|
||||
case 'upload':
|
||||
if (fileElement) {
|
||||
fileElement.click();
|
||||
}
|
||||
return;
|
||||
case 'doodle':
|
||||
onDoodleOpen();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
handleRefFileElement = (fileElement) => {
|
||||
this.fileElement = fileElement;
|
||||
};
|
||||
|
||||
renderToggleItemContents = (item) => {
|
||||
const { onChangeAdvancedOption } = this.props;
|
||||
const { name, meta, text } = item;
|
||||
|
||||
return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />;
|
||||
};
|
||||
|
||||
render () {
|
||||
const {
|
||||
acceptContentTypes,
|
||||
advancedOptions,
|
||||
contentType,
|
||||
disabled,
|
||||
allowMedia,
|
||||
allowPoll,
|
||||
hasPoll,
|
||||
onChangeAdvancedOption,
|
||||
onChangeContentType,
|
||||
onTogglePoll,
|
||||
onToggleSpoiler,
|
||||
resetFileKey,
|
||||
spoiler,
|
||||
showContentTypeChoice,
|
||||
isEditing,
|
||||
intl: { formatMessage },
|
||||
} = this.props;
|
||||
|
||||
const contentTypeItems = {
|
||||
plain: {
|
||||
icon: 'file-text',
|
||||
iconComponent: DescriptionIcon,
|
||||
name: 'text/plain',
|
||||
text: formatMessage(messages.plain),
|
||||
},
|
||||
html: {
|
||||
icon: 'code',
|
||||
iconComponent: CodeIcon,
|
||||
name: 'text/html',
|
||||
text: formatMessage(messages.html),
|
||||
},
|
||||
markdown: {
|
||||
icon: 'arrow-circle-down',
|
||||
iconComponent: MarkdownIcon,
|
||||
name: 'text/markdown',
|
||||
text: formatMessage(messages.markdown),
|
||||
},
|
||||
};
|
||||
|
||||
// The result.
|
||||
return (
|
||||
<div className='compose-form__buttons'>
|
||||
<input
|
||||
accept={acceptContentTypes}
|
||||
disabled={disabled || !allowMedia}
|
||||
key={resetFileKey}
|
||||
onChange={this.handleChangeFiles}
|
||||
ref={this.handleRefFileElement}
|
||||
type='file'
|
||||
multiple
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<DropdownContainer
|
||||
disabled={disabled || !allowMedia}
|
||||
icon='paperclip'
|
||||
iconComponent={AttachFileIcon}
|
||||
items={[
|
||||
{
|
||||
icon: 'cloud-upload',
|
||||
iconComponent: UploadFileIcon,
|
||||
name: 'upload',
|
||||
text: formatMessage(messages.upload),
|
||||
},
|
||||
{
|
||||
icon: 'paint-brush',
|
||||
iconComponent: BrushIcon,
|
||||
name: 'doodle',
|
||||
text: formatMessage(messages.doodle),
|
||||
},
|
||||
]}
|
||||
onChange={this.handleClickAttach}
|
||||
title={formatMessage(messages.attach)}
|
||||
/>
|
||||
{!!pollLimits && (
|
||||
<IconButton
|
||||
active={hasPoll}
|
||||
disabled={disabled || !allowPoll}
|
||||
icon='tasks'
|
||||
iconComponent={InsertChartIcon}
|
||||
inverted
|
||||
onClick={onTogglePoll}
|
||||
size={18}
|
||||
style={{
|
||||
height: null,
|
||||
lineHeight: null,
|
||||
}}
|
||||
title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)}
|
||||
/>
|
||||
)}
|
||||
<PrivacyDropdownContainer disabled={disabled || isEditing} />
|
||||
{showContentTypeChoice && (
|
||||
<DropdownContainer
|
||||
disabled={disabled}
|
||||
icon={(contentTypeItems[contentType.split('/')[1]] || {}).icon}
|
||||
iconComponent={(contentTypeItems[contentType.split('/')[1]] || {}).iconComponent}
|
||||
items={[
|
||||
contentTypeItems.plain,
|
||||
contentTypeItems.html,
|
||||
contentTypeItems.markdown,
|
||||
]}
|
||||
onChange={onChangeContentType}
|
||||
title={formatMessage(messages.content_type)}
|
||||
value={contentType}
|
||||
/>
|
||||
)}
|
||||
{onToggleSpoiler && (
|
||||
<TextIconButton
|
||||
active={spoiler}
|
||||
ariaControls='cw-spoiler-input'
|
||||
label='CW'
|
||||
onClick={onToggleSpoiler}
|
||||
title={formatMessage(messages.spoiler)}
|
||||
/>
|
||||
)}
|
||||
<LanguageDropdown />
|
||||
<DropdownContainer
|
||||
disabled={disabled || isEditing}
|
||||
icon='ellipsis-h'
|
||||
iconComponent={MoreHorizIcon}
|
||||
items={advancedOptions ? [
|
||||
{
|
||||
meta: formatMessage(messages.local_only_long),
|
||||
name: 'do_not_federate',
|
||||
text: formatMessage(messages.local_only_short),
|
||||
},
|
||||
{
|
||||
meta: formatMessage(messages.threaded_mode_long),
|
||||
name: 'threaded_mode',
|
||||
text: formatMessage(messages.threaded_mode_short),
|
||||
},
|
||||
] : null}
|
||||
onChange={onChangeAdvancedOption}
|
||||
renderItemContents={this.renderToggleItemContents}
|
||||
title={formatMessage(messages.advanced_options_icon_title)}
|
||||
closeOnChange={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(ComposerOptions);
|
@ -0,0 +1,55 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' },
|
||||
remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' },
|
||||
});
|
||||
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
};
|
||||
|
||||
class PollButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
active: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onClick();
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, active, disabled } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-button'>
|
||||
<IconButton
|
||||
icon='tasks'
|
||||
iconComponent={BarChart4BarsIcon}
|
||||
title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)}
|
||||
disabled={disabled}
|
||||
onClick={this.handleClick}
|
||||
className={`compose-form__poll-button-icon ${active ? 'active' : ''}`}
|
||||
size={18}
|
||||
inverted
|
||||
style={iconStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(PollButton);
|
@ -1,179 +1,163 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import {
|
||||
changePollSettings,
|
||||
changePollOption,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import AutosuggestInput from 'flavours/glitch/components/autosuggest_input';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { pollLimits } from 'flavours/glitch/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' },
|
||||
multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' },
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' },
|
||||
duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll length' },
|
||||
type: { id: 'compose_form.poll.type', defaultMessage: 'Style' },
|
||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||
});
|
||||
|
||||
class OptionIntl extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
lang: PropTypes.string,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
this.props.onChange(this.props.index, e.target.value);
|
||||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
};
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
};
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__option editable'>
|
||||
<span className={classNames('poll__input', { checkbox: isPollMultiple })} />
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={pollLimits.max_option_chars}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index < pollLimits.min_options} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
const Select = ({ label, options, value, onChange }) => {
|
||||
return (
|
||||
<label className='compose-form__poll__select'>
|
||||
<span className='compose-form__poll__select__label'>{label}</span>
|
||||
|
||||
<select className='compose-form__poll__select__value' value={value} onChange={onChange}>
|
||||
{options.map((option, i) => (
|
||||
<option key={i} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
})),
|
||||
};
|
||||
|
||||
const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||
|
||||
const handleChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollOption(index, value));
|
||||
}, [dispatch, index]);
|
||||
|
||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSuggestionsClearRequested = useCallback(() => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSuggestionSelected = useCallback((tokenStart, token, value) => {
|
||||
dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
|
||||
}, [dispatch, index]);
|
||||
|
||||
return (
|
||||
<label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}>
|
||||
<span className={classNames('poll__input', { checkbox: multipleChoice })} />
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={pollLimits.max_option_chars}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={handleChange}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||
onSuggestionSelected={handleSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
Option.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
multipleChoice: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const PollForm = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const poll = useSelector(state => state.getIn(['compose', 'poll']));
|
||||
const options = poll?.get('options');
|
||||
const expiresIn = poll?.get('expires_in');
|
||||
const isMultiple = poll?.get('multiple');
|
||||
|
||||
const handleDurationChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(value, isMultiple));
|
||||
}, [dispatch, isMultiple]);
|
||||
|
||||
const handleTypeChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(expiresIn, value === 'true'));
|
||||
}, [dispatch, expiresIn]);
|
||||
|
||||
if (poll === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Option = injectIntl(OptionIntl);
|
||||
|
||||
class PollForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
options: ImmutablePropTypes.list,
|
||||
lang: PropTypes.string,
|
||||
expiresIn: PropTypes.number,
|
||||
isMultiple: PropTypes.bool,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleAddOption = () => {
|
||||
this.props.onAddOption('');
|
||||
};
|
||||
|
||||
handleSelectDuration = e => {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleSelectMultiple = e => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true');
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
{options.size < pollLimits.max_options && (
|
||||
<label className='poll__text editable'>
|
||||
<span className={classNames('poll__input')} style={{ opacity: 0 }} />
|
||||
<button className='button button-secondary' onClick={this.handleAddOption} type='button'><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
</label>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}>
|
||||
<option value='false'>{intl.formatMessage(messages.single_choice)}</option>
|
||||
<option value='true'>{intl.formatMessage(messages.multiple_choices)}</option>
|
||||
</select>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
||||
<option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
|
||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||
</select>
|
||||
</div>
|
||||
return (
|
||||
<div className='compose-form__poll'>
|
||||
{options.map((title, i) => (
|
||||
<Option
|
||||
title={title}
|
||||
key={i}
|
||||
index={i}
|
||||
multipleChoice={isMultiple}
|
||||
autoFocus={i === 0}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className='compose-form__poll__footer'>
|
||||
<Select label={intl.formatMessage(messages.duration)} options={[
|
||||
{ value: 300, label: intl.formatMessage(messages.minutes, { number: 5 })},
|
||||
{ value: 1800, label: intl.formatMessage(messages.minutes, { number: 30 })},
|
||||
{ value: 3600, label: intl.formatMessage(messages.hours, { number: 1 })},
|
||||
{ value: 21600, label: intl.formatMessage(messages.hours, { number: 6 })},
|
||||
{ value: 43200, label: intl.formatMessage(messages.hours, { number: 12 })},
|
||||
{ value: 86400, label: intl.formatMessage(messages.days, { number: 1 })},
|
||||
{ value: 259200, label: intl.formatMessage(messages.days, { number: 3 })},
|
||||
{ value: 604800, label: intl.formatMessage(messages.days, { number: 7 })},
|
||||
]} value={expiresIn} onChange={handleDurationChange} />
|
||||
|
||||
<div className='compose-form__poll__footer__sep' />
|
||||
|
||||
<Select label={intl.formatMessage(messages.type)} options={[
|
||||
{ value: false, label: intl.formatMessage(messages.singleChoice) },
|
||||
{ value: true, label: intl.formatMessage(messages.multipleChoice) },
|
||||
]} value={isMultiple} onChange={handleTypeChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(PollForm);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,114 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react';
|
||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
publish: {
|
||||
defaultMessage: 'Publish',
|
||||
id: 'compose_form.publish',
|
||||
},
|
||||
publishLoud: {
|
||||
defaultMessage: '{publish}!',
|
||||
id: 'compose_form.publish_loud',
|
||||
},
|
||||
saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' },
|
||||
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
|
||||
private: { id: 'privacy.private.short', defaultMessage: 'Followers only' },
|
||||
direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
|
||||
});
|
||||
|
||||
class Publisher extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
intl: PropTypes.object.isRequired,
|
||||
onSecondarySubmit: PropTypes.func,
|
||||
privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']),
|
||||
sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']),
|
||||
isEditing: PropTypes.bool,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props;
|
||||
|
||||
const privacyIcons = {
|
||||
direct: {
|
||||
id: 'envelope',
|
||||
icon: MailIcon,
|
||||
},
|
||||
private: {
|
||||
id: 'lock',
|
||||
icon: LockIcon,
|
||||
},
|
||||
public: {
|
||||
id: 'globe',
|
||||
icon: PublicIcon,
|
||||
},
|
||||
unlisted: {
|
||||
id: 'unlock',
|
||||
icon: LockOpenIcon,
|
||||
},
|
||||
};
|
||||
|
||||
let publishText;
|
||||
if (isEditing) {
|
||||
publishText = intl.formatMessage(messages.saveChanges);
|
||||
} else if (privacy === 'private' || privacy === 'direct') {
|
||||
const icon = privacyIcons[privacy];
|
||||
publishText = (
|
||||
<span>
|
||||
<Icon {...icon} /> {intl.formatMessage(messages.publish)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish);
|
||||
}
|
||||
|
||||
const privacyNames = {
|
||||
public: messages.public,
|
||||
unlisted: messages.unlisted,
|
||||
private: messages.private,
|
||||
direct: messages.direct,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='compose-form__publish'>
|
||||
{sideArm && !isEditing && sideArm !== 'none' && (
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
className='side_arm'
|
||||
disabled={disabled}
|
||||
onClick={onSecondarySubmit}
|
||||
style={{ padding: null }}
|
||||
text={<Icon {...privacyIcons[sideArm]} />}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[sideArm])}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className='compose-form__publish-button-wrapper'>
|
||||
<Button
|
||||
className='primary'
|
||||
type='submit'
|
||||
text={publishText}
|
||||
title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage(privacyNames[privacy])}`}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(Publisher);
|
@ -1,75 +1,47 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
|
||||
export const ReplyIndicator = () => {
|
||||
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||
const status = useSelector(state => state.getIn(['statuses', inReplyToId]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
class ReplyIndicator extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__line' />
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</Permalink>
|
||||
|
||||
<a href={status.getIn(['account', 'url'])} onClick={this.handleAccountClick} className='reply-indicator__display-name' target='_blank' rel='noopener noreferrer'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
<DisplayName account={status.get('account')} inline />
|
||||
</a>
|
||||
</div>
|
||||
<div className='reply-indicator__main'>
|
||||
<Permalink href={account.get('url')} to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<DisplayName account={account} />
|
||||
</Permalink>
|
||||
|
||||
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='reply-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withOptionalRouter(injectIntl(ReplyIndicator));
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
|
||||
import MailIcon from '@/material-icons/400-24px/mail.svg?react';
|
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
|
||||
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
|
||||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
const messages = defineMessages({
|
||||
public: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' },
|
||||
private: { id: 'privacy.private.short', defaultMessage: 'Followers' },
|
||||
direct: { id: 'privacy.direct.short', defaultMessage: 'Specific people' },
|
||||
});
|
||||
|
||||
export const SecondaryPrivacyButton = ({ disabled, privacy, isEditing, onClick }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (isEditing || !privacy || privacy === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const privacyProps = {
|
||||
direct: { icon: 'envelope', iconComponent: MailIcon, title: messages.direct },
|
||||
private: { icon: 'lock', iconComponent: LockIcon, title: messages.private },
|
||||
public: { icon: 'globe', iconComponent: PublicIcon, title: messages.public },
|
||||
unlisted: { icon: 'unlock', iconComponent: QuietTimeIcon, title: messages.unlisted },
|
||||
};
|
||||
|
||||
return (
|
||||
<Button className='secondary-post-button' disabled={disabled} onClick={onClick} title={intl.formatMessage(privacyProps[privacy].title)}>
|
||||
<Icon id={privacyProps[privacy].id} icon={privacyProps[privacy].iconComponent} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
SecondaryPrivacyButton.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
privacy: PropTypes.string,
|
||||
isEditing: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
@ -0,0 +1,58 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { changeComposeSensitivity } from 'flavours/glitch/actions/compose';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: {
|
||||
id: 'compose_form.sensitive.marked',
|
||||
defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
|
||||
},
|
||||
unmarked: {
|
||||
id: 'compose_form.sensitive.unmarked',
|
||||
defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
|
||||
},
|
||||
});
|
||||
|
||||
export const SensitiveButton = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const spoilersAlwaysOn = useAppSelector((state) => state.getIn(['local_settings', 'always_show_spoilers_field']));
|
||||
const spoilerText = useAppSelector((state) => state.getIn(['compose', 'spoiler_text']));
|
||||
const sensitive = useAppSelector((state) => state.getIn(['compose', 'sensitive']));
|
||||
const spoiler = useAppSelector((state) => state.getIn(['compose', 'spoiler']));
|
||||
const mediaCount = useAppSelector((state) => state.getIn(['compose', 'media_attachments']).size);
|
||||
const disabled = spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler;
|
||||
|
||||
const active = sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0);
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(changeComposeSensitivity());
|
||||
}, [dispatch]);
|
||||
|
||||
return (
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
|
||||
<input
|
||||
name='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={handleClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='compose_form.sensitive.hide'
|
||||
defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
|
||||
values={{ count: mediaCount }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,59 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import ForumIcon from '@/material-icons/400-24px/forum.svg?react';
|
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
|
||||
|
||||
const messages = defineMessages({
|
||||
localOnly: {
|
||||
defaultMessage: 'This post is local-only',
|
||||
id: 'advanced_options.local-only.tooltip',
|
||||
},
|
||||
threadedMode: {
|
||||
defaultMessage: 'Threaded mode enabled',
|
||||
id: 'advanced_options.threaded_mode.tooltip',
|
||||
},
|
||||
});
|
||||
|
||||
// We use an array of tuples here instead of an object because it
|
||||
// preserves order.
|
||||
const iconMap = [
|
||||
['do_not_federate', 'home', HomeIcon, messages.localOnly],
|
||||
['threaded_mode', 'comments', ForumIcon, messages.threadedMode],
|
||||
];
|
||||
|
||||
class TextareaIcons extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
advancedOptions: ImmutablePropTypes.map,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { advancedOptions, intl } = this.props;
|
||||
return (
|
||||
<div className='compose-form__textarea-icons'>
|
||||
{advancedOptions && iconMap.map(
|
||||
([key, icon, iconComponent, message]) => advancedOptions.get(key) && (
|
||||
<span
|
||||
className='textarea_icon'
|
||||
key={key}
|
||||
title={intl.formatMessage(message)}
|
||||
>
|
||||
<Icon id={icon} icon={iconComponent} />
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(TextareaIcons);
|
@ -0,0 +1,41 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import QuickreplyIcon from '@/material-icons/400-24px/quickreply.svg?react';
|
||||
import { changeComposeAdvancedOption } from 'flavours/glitch/actions/compose';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
enable_threaded_mode: { id: 'compose.enable_threaded_mode', defaultMessage: 'Enable threaded more' },
|
||||
disable_threaded_mode: { id: 'compose.disable_threaded_mode', defaultMessage: 'Disable threaded more' },
|
||||
});
|
||||
|
||||
export const ThreadModeButton = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const isEditing = useAppSelector((state) => state.getIn(['compose', 'id']) !== null);
|
||||
const active = useAppSelector((state) => state.getIn(['compose', 'advanced_options', 'threaded_mode']));
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
dispatch(changeComposeAdvancedOption('threaded_mode', !active));
|
||||
}, [active, dispatch]);
|
||||
|
||||
const title = intl.formatMessage(active ? messages.disable_threaded_mode : messages.enable_threaded_mode);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
disabled={isEditing}
|
||||
icon=''
|
||||
onClick={handleClick}
|
||||
iconComponent={QuickreplyIcon}
|
||||
title={title}
|
||||
active={active}
|
||||
size={18}
|
||||
inverted
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const mapStateToProps = state => ({
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']),
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const iconStyle = {
|
||||
height: null,
|
||||
lineHeight: '27px',
|
||||
};
|
||||
|
||||
class UploadButton extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
onSelectFile: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
resetFileKey: PropTypes.number,
|
||||
acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleChange = (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
this.props.onSelectFile(e.target.files);
|
||||
}
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.fileElement.click();
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.fileElement = c;
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, resetFileKey, disabled, acceptContentTypes } = this.props;
|
||||
|
||||
const message = intl.formatMessage(messages.upload);
|
||||
|
||||
return (
|
||||
<div className='compose-form__upload-button'>
|
||||
<IconButton icon='paperclip' iconComponent={PhotoLibraryIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} />
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{message}</span>
|
||||
<input
|
||||
key={resetFileKey}
|
||||
ref={this.setRef}
|
||||
type='file'
|
||||
multiple
|
||||
accept={acceptContentTypes.toArray().join(',')}
|
||||
onChange={this.handleChange}
|
||||
disabled={disabled}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps)(injectIntl(UploadButton));
|
@ -1,14 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal, closeModal } from 'flavours/glitch/actions/modal';
|
||||
import { isUserTouching } from 'flavours/glitch/is_mobile';
|
||||
|
||||
import Dropdown from '../components/dropdown';
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
isUserTouching,
|
||||
onModalOpen: props => dispatch(openModal({ modalType: 'ACTIONS', modalProps: props })),
|
||||
onModalClose: () => dispatch(closeModal({ modalType: undefined, ignoreFocus: false })),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(Dropdown);
|
@ -1,42 +0,0 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
||||
|
||||
import Header from '../components/header';
|
||||
|
||||
const messages = defineMessages({
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
columns: state.getIn(['settings', 'columns']),
|
||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||
showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onSettingsClick (e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dispatch(openModal({ modalType: 'SETTINGS', modalProps: {} }));
|
||||
},
|
||||
onLogout () {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Header));
|
@ -1,36 +0,0 @@
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
import { logOut } from 'flavours/glitch/utils/log_out';
|
||||
|
||||
import { me } from '../../../initial_state';
|
||||
import NavigationBar from '../components/navigation_bar';
|
||||
|
||||
const messages = defineMessages({
|
||||
logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
|
||||
logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
account: state.getIn(['accounts', me]),
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
onLogout () {
|
||||
dispatch(openModal({
|
||||
modalType: 'CONFIRM',
|
||||
modalProps: {
|
||||
message: intl.formatMessage(messages.logoutMessage),
|
||||
confirm: intl.formatMessage(messages.logoutConfirm),
|
||||
closeWhenConfirm: false,
|
||||
onConfirm: () => logOut(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
|
@ -1,56 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
changeComposeAdvancedOption,
|
||||
changeComposeContentType,
|
||||
addPoll,
|
||||
removePoll,
|
||||
} from 'flavours/glitch/actions/compose';
|
||||
import { openModal } from 'flavours/glitch/actions/modal';
|
||||
|
||||
import Options from '../components/options';
|
||||
|
||||
function mapStateToProps (state) {
|
||||
const poll = state.getIn(['compose', 'poll']);
|
||||
const media = state.getIn(['compose', 'media_attachments']);
|
||||
const pending_media = state.getIn(['compose', 'pending_media_attachments']);
|
||||
return {
|
||||
acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
hasPoll: !!poll,
|
||||
allowMedia: !poll && (media ? media.size + pending_media < 4 && !media.some(item => ['video', 'audio'].includes(item.get('type'))) : pending_media < 4),
|
||||
allowPoll: !(media && !!media.size),
|
||||
showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']),
|
||||
contentType: state.getIn(['compose', 'content_type']),
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
|
||||
onChangeAdvancedOption(option, value) {
|
||||
dispatch(changeComposeAdvancedOption(option, value));
|
||||
},
|
||||
|
||||
onChangeContentType(value) {
|
||||
dispatch(changeComposeContentType(value));
|
||||
},
|
||||
|
||||
onTogglePoll() {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(removePoll());
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
onDoodleOpen() {
|
||||
dispatch(openModal({
|
||||
modalType: 'DOODLE',
|
||||
modalProps: { noEsc: true, noClose: true },
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Options);
|
@ -0,0 +1,25 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { addPoll, removePoll } from '../../../actions/compose';
|
||||
import PollButton from '../components/poll_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0),
|
||||
active: state.getIn(['compose', 'poll']) !== null,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch((_, getState) => {
|
||||
if (getState().getIn(['compose', 'poll'])) {
|
||||
dispatch(removePoll());
|
||||
} else {
|
||||
dispatch(addPoll());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollButton);
|
@ -1,53 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
addPollOption,
|
||||
removePollOption,
|
||||
changePollOption,
|
||||
changePollSettings,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from '../../../actions/compose';
|
||||
import PollForm from '../components/poll_form';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
suggestions: state.getIn(['compose', 'suggestions']),
|
||||
options: state.getIn(['compose', 'poll', 'options']),
|
||||
lang: state.getIn(['compose', 'language']),
|
||||
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
|
||||
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
onAddOption(title) {
|
||||
dispatch(addPollOption(title));
|
||||
},
|
||||
|
||||
onRemoveOption(index) {
|
||||
dispatch(removePollOption(index));
|
||||
},
|
||||
|
||||
onChangeOption(index, title) {
|
||||
dispatch(changePollOption(index, title));
|
||||
},
|
||||
|
||||
onChangeSettings(expiresIn, isMultiple) {
|
||||
dispatch(changePollSettings(expiresIn, isMultiple));
|
||||
},
|
||||
|
||||
onClearSuggestions () {
|
||||
dispatch(clearComposeSuggestions());
|
||||
},
|
||||
|
||||
onFetchSuggestions (token) {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
},
|
||||
|
||||
onSuggestionSelected (position, token, accountId, path) {
|
||||
dispatch(selectComposeSuggestion(position, token, accountId, path));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PollForm);
|
@ -1,36 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { cancelReplyCompose } from '../../../actions/compose';
|
||||
import { makeGetStatus } from '../../../selectors';
|
||||
import ReplyIndicator from '../components/reply_indicator';
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = state => {
|
||||
let statusId = state.getIn(['compose', 'id'], null);
|
||||
let editing = true;
|
||||
|
||||
if (statusId === null) {
|
||||
statusId = state.getIn(['compose', 'in_reply_to']);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
return {
|
||||
status: getStatus(state, { id: statusId }),
|
||||
editing,
|
||||
};
|
||||
};
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onCancel () {
|
||||
dispatch(cancelReplyCompose());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator);
|
@ -1,77 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { changeComposeSensitivity } from 'flavours/glitch/actions/compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: {
|
||||
id: 'compose_form.sensitive.marked',
|
||||
defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}',
|
||||
},
|
||||
unmarked: {
|
||||
id: 'compose_form.sensitive.unmarked',
|
||||
defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}',
|
||||
},
|
||||
});
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']);
|
||||
const spoilerText = state.getIn(['compose', 'spoiler_text']);
|
||||
return {
|
||||
active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0),
|
||||
disabled: state.getIn(['compose', 'spoiler']),
|
||||
mediaCount: state.getIn(['compose', 'media_attachments']).size,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch(changeComposeSensitivity());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
class SensitiveButton extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
mediaCount: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { active, disabled, mediaCount, onClick, intl } = this.props;
|
||||
|
||||
return (
|
||||
<div className='compose-form__sensitive-button'>
|
||||
<label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}>
|
||||
<input
|
||||
name='mark-sensitive'
|
||||
type='checkbox'
|
||||
checked={active}
|
||||
onChange={onClick}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='compose_form.sensitive.hide'
|
||||
defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}'
|
||||
values={{ count: mediaCount }}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton));
|
@ -0,0 +1,32 @@
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import WarningIcon from '@/material-icons/400-24px/warning.svg?react';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
|
||||
import { changeComposeSpoilerness } from '../../../actions/compose';
|
||||
|
||||
const messages = defineMessages({
|
||||
marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' },
|
||||
unmarked: { id: 'compose_form.spoiler.unmarked', defaultMessage: 'Text is not hidden' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { intl }) => ({
|
||||
iconComponent: WarningIcon,
|
||||
title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked),
|
||||
active: state.getIn(['compose', 'spoiler']),
|
||||
ariaControls: 'cw-spoiler-input',
|
||||
size: 18,
|
||||
inverted: true,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onClick () {
|
||||
dispatch(changeComposeSpoilerness());
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton));
|
@ -0,0 +1,19 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { uploadCompose } from '../../../actions/compose';
|
||||
import UploadButton from '../components/upload_button';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))),
|
||||
resetFileKey: state.getIn(['compose', 'resetFileKey']),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
|
||||
onSelectFile (files) {
|
||||
dispatch(uploadCompose(files));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UploadButton);
|
@ -1,21 +1,15 @@
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import ComposeFormContainer from '../../compose/containers/compose_form_container';
|
||||
import LoadingBarContainer from '../../ui/containers/loading_bar_container';
|
||||
import ModalContainer from '../../ui/containers/modal_container';
|
||||
import NotificationsContainer from '../../ui/containers/notifications_container';
|
||||
|
||||
export default class Compose extends PureComponent {
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<ComposeFormContainer autoFocus />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container';
|
||||
import LoadingBarContainer from 'flavours/glitch/features/ui/containers/loading_bar_container';
|
||||
import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container';
|
||||
import NotificationsContainer from 'flavours/glitch/features/ui/containers/notifications_container';
|
||||
|
||||
const Compose = () => (
|
||||
<>
|
||||
<ComposeFormContainer autoFocus withoutNavigation />
|
||||
<NotificationsContainer />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
</>
|
||||
);
|
||||
|
||||
export default Compose;
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
<svg width="5" height="80" viewBox="0 0 5 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_253_1286)">
|
||||
<rect width="5" height="80" fill="url(#paint0_linear_253_1286)"/>
|
||||
<line x1="-0.860365" y1="6.80136" x2="10.6078" y2="-1.22871" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="14.8314" x2="10.6078" y2="6.80132" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="22.8615" x2="10.6078" y2="14.8314" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="30.8916" x2="10.6078" y2="22.8615" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="38.9216" x2="10.6078" y2="30.8915" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="46.9517" x2="10.6078" y2="38.9216" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="54.9818" x2="10.6078" y2="46.9517" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="63.0118" x2="10.6078" y2="54.9817" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="71.0419" x2="10.6078" y2="63.0118" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="79.072" x2="10.6078" y2="71.0419" stroke="black" stroke-width="3"/>
|
||||
<line x1="-0.860365" y1="87.102" x2="10.6078" y2="79.072" stroke="black" stroke-width="3"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_253_1286" x1="2.5" y1="0" x2="2.5" y2="80" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FEC84B"/>
|
||||
<stop offset="1" stop-color="#F79009"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_253_1286">
|
||||
<rect width="5" height="80" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
@ -1,26 +1,18 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
|
||||
import { length } from 'stringz';
|
||||
|
||||
export default class CharacterCounter extends PureComponent {
|
||||
export const CharacterCounter = ({ text, max }) => {
|
||||
const diff = max - length(text);
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
checkRemainingText (diff) {
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
if (diff < 0) {
|
||||
return <span className='character-counter character-counter--over'>{diff}</span>;
|
||||
}
|
||||
|
||||
render () {
|
||||
const diff = this.props.max - length(this.props.text);
|
||||
return this.checkRemainingText(diff);
|
||||
}
|
||||
return <span className='character-counter'>{diff}</span>;
|
||||
};
|
||||
|
||||
}
|
||||
CharacterCounter.propTypes = {
|
||||
text: PropTypes.string.isRequired,
|
||||
max: PropTypes.number.isRequired,
|
||||
};
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import BarChart4BarsIcon from 'mastodon/../material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||
import PhotoLibraryIcon from 'mastodon/../material-icons/400-24px/photo_library.svg?react';
|
||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
export const EditIndicator = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const id = useSelector(state => state.getIn(['compose', 'id']));
|
||||
const status = useSelector(state => state.getIn(['statuses', id]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
return (
|
||||
<div className='edit-indicator'>
|
||||
<div className='edit-indicator__header'>
|
||||
<div className='edit-indicator__display-name'>
|
||||
<Link to={`/@${account.get('acct')}`}>@{account.get('acct')}</Link>
|
||||
·
|
||||
<Link to={`/@${account.get('acct')}/${status.get('id')}`}><RelativeTimestamp timestamp={status.get('created_at')} /></Link>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__cancel'>
|
||||
<IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='edit-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,50 +1,36 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import CloseIcon from 'mastodon/../material-icons/400-24px/close.svg?react';
|
||||
import { cancelReplyCompose } from 'mastodon/actions/compose';
|
||||
import Account from 'mastodon/components/account';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { me } from 'mastodon/initial_state';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { ActionBar } from './action_bar';
|
||||
|
||||
import ActionBar from './action_bar';
|
||||
|
||||
export default class NavigationBar extends ImmutablePureComponent {
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.record.isRequired,
|
||||
onLogout: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
export const NavigationBar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const account = useSelector(state => state.getIn(['accounts', me]));
|
||||
const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to']));
|
||||
|
||||
render () {
|
||||
const username = this.props.account.get('acct');
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Link to={`/@${username}`}>
|
||||
<span style={{ display: 'none' }}>{username}</span>
|
||||
<Avatar account={this.props.account} size={46} />
|
||||
</Link>
|
||||
const handleCancelClick = useCallback(() => {
|
||||
dispatch(cancelReplyCompose());
|
||||
}, [dispatch]);
|
||||
|
||||
<div className='navigation-bar__profile'>
|
||||
<span>
|
||||
<Link to={`/@${username}`}>
|
||||
<strong className='navigation-bar__profile-account'>@{username}</strong>
|
||||
</Link>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='navigation-bar__actions'>
|
||||
<ActionBar account={this.props.account} onLogout={this.props.onLogout} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
return (
|
||||
<div className='navigation-bar'>
|
||||
<Account account={account} minimal />
|
||||
{isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,190 +1,162 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { PureComponent } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import AddIcon from '@/material-icons/400-24px/add.svg?react';
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import {
|
||||
changePollSettings,
|
||||
changePollOption,
|
||||
clearComposeSuggestions,
|
||||
fetchComposeSuggestions,
|
||||
selectComposeSuggestion,
|
||||
} from 'mastodon/actions/compose';
|
||||
import AutosuggestInput from 'mastodon/components/autosuggest_input';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { pollMinOptions } from 'mastodon/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' },
|
||||
poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' },
|
||||
option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' },
|
||||
add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' },
|
||||
remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' },
|
||||
duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll length' },
|
||||
type: { id: 'compose_form.poll.type', defaultMessage: 'Style' },
|
||||
switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' },
|
||||
switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' },
|
||||
minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' },
|
||||
hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' },
|
||||
days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' },
|
||||
singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' },
|
||||
multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' },
|
||||
});
|
||||
|
||||
class OptionIntl extends PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
lang: PropTypes.string,
|
||||
index: PropTypes.number.isRequired,
|
||||
isPollMultiple: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onToggleMultiple: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleOptionTitleChange = e => {
|
||||
this.props.onChange(this.props.index, e.target.value);
|
||||
};
|
||||
|
||||
handleOptionRemove = () => {
|
||||
this.props.onRemove(this.props.index);
|
||||
};
|
||||
|
||||
|
||||
handleToggleMultiple = e => {
|
||||
this.props.onToggleMultiple();
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
handleCheckboxKeypress = e => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
this.handleToggleMultiple(e);
|
||||
}
|
||||
};
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
this.props.onClearSuggestions();
|
||||
};
|
||||
|
||||
onSuggestionsFetchRequested = (token) => {
|
||||
this.props.onFetchSuggestions(token);
|
||||
};
|
||||
|
||||
onSuggestionSelected = (tokenStart, token, value) => {
|
||||
this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<label className='poll__option editable'>
|
||||
<span
|
||||
className={classNames('poll__input', { checkbox: isPollMultiple })}
|
||||
onClick={this.handleToggleMultiple}
|
||||
onKeyPress={this.handleCheckboxKeypress}
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)}
|
||||
/>
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={100}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={this.handleOptionTitleChange}
|
||||
suggestions={this.props.suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className='poll__cancel'>
|
||||
<IconButton disabled={index < pollMinOptions} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
const Select = ({ label, options, value, onChange }) => {
|
||||
return (
|
||||
<label className='compose-form__poll__select'>
|
||||
<span className='compose-form__poll__select__label'>{label}</span>
|
||||
|
||||
<select className='compose-form__poll__select__value' value={value} onChange={onChange}>
|
||||
{options.map((option, i) => (
|
||||
<option key={i} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
Select.propTypes = {
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
onChange: PropTypes.func,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.node,
|
||||
value: PropTypes.any,
|
||||
})),
|
||||
};
|
||||
|
||||
const Option = ({ multipleChoice, index, title, autoFocus }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const suggestions = useSelector(state => state.getIn(['compose', 'suggestions']));
|
||||
const lang = useSelector(state => state.getIn(['compose', 'language']));
|
||||
|
||||
const handleChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollOption(index, value));
|
||||
}, [dispatch, index]);
|
||||
|
||||
const handleSuggestionsFetchRequested = useCallback(token => {
|
||||
dispatch(fetchComposeSuggestions(token));
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSuggestionsClearRequested = useCallback(() => {
|
||||
dispatch(clearComposeSuggestions());
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSuggestionSelected = useCallback((tokenStart, token, value) => {
|
||||
dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index]));
|
||||
}, [dispatch, index]);
|
||||
|
||||
return (
|
||||
<label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}>
|
||||
<span className={classNames('poll__input', { checkbox: multipleChoice })} />
|
||||
|
||||
<AutosuggestInput
|
||||
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
|
||||
maxLength={100}
|
||||
value={title}
|
||||
lang={lang}
|
||||
spellCheck
|
||||
onChange={handleChange}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={handleSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={handleSuggestionsClearRequested}
|
||||
onSuggestionSelected={handleSuggestionSelected}
|
||||
searchTokens={[':']}
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
Option.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
index: PropTypes.number.isRequired,
|
||||
multipleChoice: PropTypes.bool,
|
||||
autoFocus: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const PollForm = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const poll = useSelector(state => state.getIn(['compose', 'poll']));
|
||||
const options = poll?.get('options');
|
||||
const expiresIn = poll?.get('expires_in');
|
||||
const isMultiple = poll?.get('multiple');
|
||||
|
||||
const handleDurationChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(value, isMultiple));
|
||||
}, [dispatch, isMultiple]);
|
||||
|
||||
const handleTypeChange = useCallback(({ target: { value } }) => {
|
||||
dispatch(changePollSettings(expiresIn, value === 'true'));
|
||||
}, [dispatch, expiresIn]);
|
||||
|
||||
if (poll === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Option = injectIntl(OptionIntl);
|
||||
|
||||
class PollForm extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
options: ImmutablePropTypes.list,
|
||||
lang: PropTypes.string,
|
||||
expiresIn: PropTypes.number,
|
||||
isMultiple: PropTypes.bool,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onAddOption: PropTypes.func.isRequired,
|
||||
onRemoveOption: PropTypes.func.isRequired,
|
||||
onChangeSettings: PropTypes.func.isRequired,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
onClearSuggestions: PropTypes.func.isRequired,
|
||||
onFetchSuggestions: PropTypes.func.isRequired,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
handleAddOption = () => {
|
||||
this.props.onAddOption('');
|
||||
};
|
||||
|
||||
handleSelectDuration = e => {
|
||||
this.props.onChangeSettings(e.target.value, this.props.isMultiple);
|
||||
};
|
||||
|
||||
handleToggleMultiple = () => {
|
||||
this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
|
||||
|
||||
if (!options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const autoFocusIndex = options.indexOf('');
|
||||
|
||||
return (
|
||||
<div className='compose-form__poll-wrapper'>
|
||||
<ul>
|
||||
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
|
||||
</ul>
|
||||
|
||||
<div className='poll__footer'>
|
||||
<button type='button' disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button>
|
||||
|
||||
{/* eslint-disable-next-line jsx-a11y/no-onchange */}
|
||||
<select value={expiresIn} onChange={this.handleSelectDuration}>
|
||||
<option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option>
|
||||
<option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option>
|
||||
<option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option>
|
||||
<option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option>
|
||||
<option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option>
|
||||
<option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option>
|
||||
<option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option>
|
||||
<option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option>
|
||||
</select>
|
||||
</div>
|
||||
return (
|
||||
<div className='compose-form__poll'>
|
||||
{options.map((title, i) => (
|
||||
<Option
|
||||
title={title}
|
||||
key={i}
|
||||
index={i}
|
||||
multipleChoice={isMultiple}
|
||||
autoFocus={i === 0}
|
||||
/>
|
||||
))}
|
||||
|
||||
<div className='compose-form__poll__footer'>
|
||||
<Select label={intl.formatMessage(messages.duration)} options={[
|
||||
{ value: 300, label: intl.formatMessage(messages.minutes, { number: 5 })},
|
||||
{ value: 1800, label: intl.formatMessage(messages.minutes, { number: 30 })},
|
||||
{ value: 3600, label: intl.formatMessage(messages.hours, { number: 1 })},
|
||||
{ value: 21600, label: intl.formatMessage(messages.hours, { number: 6 })},
|
||||
{ value: 43200, label: intl.formatMessage(messages.hours, { number: 12 })},
|
||||
{ value: 86400, label: intl.formatMessage(messages.days, { number: 1 })},
|
||||
{ value: 259200, label: intl.formatMessage(messages.days, { number: 3 })},
|
||||
{ value: 604800, label: intl.formatMessage(messages.days, { number: 7 })},
|
||||
]} value={expiresIn} onChange={handleDurationChange} />
|
||||
|
||||
<div className='compose-form__poll__footer__sep' />
|
||||
|
||||
<Select label={intl.formatMessage(messages.type)} options={[
|
||||
{ value: false, label: intl.formatMessage(messages.singleChoice) },
|
||||
{ value: true, label: intl.formatMessage(messages.multipleChoice) },
|
||||
]} value={isMultiple} onChange={handleTypeChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(PollForm);
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,74 +1,48 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import CloseIcon from '@/material-icons/400-24px/close.svg?react';
|
||||
import AttachmentList from 'mastodon/components/attachment_list';
|
||||
import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router';
|
||||
import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react';
|
||||
import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react';
|
||||
import { Avatar } from 'mastodon/components/avatar';
|
||||
import { DisplayName } from 'mastodon/components/display_name';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
export const ReplyIndicator = () => {
|
||||
const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to']));
|
||||
const status = useSelector(state => state.getIn(['statuses', inReplyToId]));
|
||||
const account = useSelector(state => state.getIn(['accounts', status?.get('account')]));
|
||||
|
||||
const messages = defineMessages({
|
||||
cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' },
|
||||
});
|
||||
|
||||
class ReplyIndicator extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
status: ImmutablePropTypes.map,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
...WithOptionalRouterPropTypes,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
this.props.onCancel();
|
||||
};
|
||||
|
||||
handleAccountClick = (e) => {
|
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
}
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
|
||||
const content = { __html: status.get('contentHtml') };
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__line' />
|
||||
|
||||
return (
|
||||
<div className='reply-indicator'>
|
||||
<div className='reply-indicator__header'>
|
||||
<div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div>
|
||||
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'>
|
||||
<Avatar account={account} size={46} />
|
||||
</Link>
|
||||
|
||||
<a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'>
|
||||
<div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div>
|
||||
<DisplayName account={status.get('account')} />
|
||||
</a>
|
||||
</div>
|
||||
<div className='reply-indicator__main'>
|
||||
<Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'>
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} />
|
||||
|
||||
{status.get('media_attachments').size > 0 && (
|
||||
<AttachmentList
|
||||
compact
|
||||
media={status.get('media_attachments')}
|
||||
/>
|
||||
{(status.get('poll') || status.get('media_attachments').size > 0) && (
|
||||
<div className='reply-indicator__attachments'>
|
||||
{status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>}
|
||||
{status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withOptionalRouter(injectIntl(ReplyIndicator));
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue