Merge branch 'ts' into 'develop'
TypeScript, React.FC See merge request soapbox-pub/soapbox-fe!1592
This commit is contained in:
commit
2d5454a93f
|
@ -1,17 +1,17 @@
|
|||
import Portal from '@reach/portal';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
import AutosuggestAccount from '../features/compose/components/autosuggest_account';
|
||||
import { isRtl } from '../rtl';
|
||||
|
||||
import AutosuggestEmoji from './autosuggest_emoji';
|
||||
import AutosuggestEmoji, { Emoji } from './autosuggest_emoji';
|
||||
|
||||
const textAtCursorMatchesToken = (str, caretPosition) => {
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
|
||||
const textAtCursorMatchesToken = (str: string, caretPosition: number) => {
|
||||
let word;
|
||||
|
||||
const left = str.slice(0, caretPosition).search(/\S+$/);
|
||||
|
@ -36,25 +36,28 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||
}
|
||||
};
|
||||
|
||||
export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||
interface IAutosuggesteTextarea {
|
||||
id?: string,
|
||||
value: string,
|
||||
suggestions: ImmutableList<string>,
|
||||
disabled: boolean,
|
||||
placeholder: string,
|
||||
onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void,
|
||||
onSuggestionsClearRequested: () => void,
|
||||
onSuggestionsFetchRequested: (token: string | number) => void,
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement>,
|
||||
onKeyUp: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement>,
|
||||
onPaste: (files: FileList) => void,
|
||||
autoFocus: boolean,
|
||||
onFocus: () => void,
|
||||
onBlur?: () => void,
|
||||
condensed?: boolean,
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
condensed: PropTypes.bool,
|
||||
};
|
||||
class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea> {
|
||||
|
||||
textarea: HTMLTextAreaElement | null = null;
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: true,
|
||||
|
@ -68,7 +71,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
tokenStart: 0,
|
||||
};
|
||||
|
||||
onChange = (e) => {
|
||||
onChange: React.ChangeEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
const [tokenStart, token] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
|
@ -82,7 +85,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
this.props.onChange(e);
|
||||
}
|
||||
|
||||
onKeyDown = (e) => {
|
||||
onKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
|
||||
|
@ -91,7 +94,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
return;
|
||||
}
|
||||
|
||||
if (e.which === 229 || e.isComposing) {
|
||||
if (e.which === 229 || (e as any).isComposing) {
|
||||
// Ignore key events during text composition
|
||||
// e.key may be a name of the physical key even in this case (e.x. Safari / Chrome on Mac)
|
||||
return;
|
||||
|
@ -100,7 +103,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
switch (e.key) {
|
||||
case 'Escape':
|
||||
if (suggestions.size === 0 || suggestionsHidden) {
|
||||
document.querySelector('.ui').parentElement.focus();
|
||||
document.querySelector('.ui')?.parentElement?.focus();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.setState({ suggestionsHidden: true });
|
||||
|
@ -156,14 +159,14 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
onSuggestionClick = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
onSuggestionClick: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index') as any);
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea.focus();
|
||||
this.textarea?.focus();
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
shouldComponentUpdate(nextProps: IAutosuggesteTextarea, nextState: any) {
|
||||
// Skip updating when only the lastToken changes so the
|
||||
// cursor doesn't jump around due to re-rendering unnecessarily
|
||||
const lastTokenUpdated = this.state.lastToken !== nextState.lastToken;
|
||||
|
@ -172,29 +175,29 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
if (lastTokenUpdated && !valueUpdated) {
|
||||
return false;
|
||||
} else {
|
||||
return super.shouldComponentUpdate(nextProps, nextState);
|
||||
return super.shouldComponentUpdate!(nextProps, nextState, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
componentDidUpdate(prevProps: IAutosuggesteTextarea, prevState: any) {
|
||||
const { suggestions } = this.props;
|
||||
if (suggestions !== prevProps.suggestions && suggestions.size > 0 && prevState.suggestionsHidden && prevState.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
setTextarea = (c) => {
|
||||
setTextarea: React.Ref<HTMLTextAreaElement> = (c) => {
|
||||
this.textarea = c;
|
||||
}
|
||||
|
||||
onPaste = (e) => {
|
||||
onPaste: React.ClipboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
renderSuggestion = (suggestion, i) => {
|
||||
renderSuggestion = (suggestion: string | Emoji, i: number) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
let inner, key;
|
||||
|
||||
|
@ -212,7 +215,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
return (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex='0'
|
||||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={classNames({
|
||||
|
@ -272,7 +275,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
style={style}
|
||||
style={style as any}
|
||||
aria-autocomplete='list'
|
||||
/>
|
||||
</label>
|
||||
|
@ -297,3 +300,5 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
export default AutosuggestTextarea;
|
|
@ -18,7 +18,7 @@ let id = 0;
|
|||
export interface MenuItem {
|
||||
action?: React.EventHandler<React.KeyboardEvent | React.MouseEvent>,
|
||||
middleClick?: React.EventHandler<React.MouseEvent>,
|
||||
text: string | JSX.Element,
|
||||
text: string,
|
||||
href?: string,
|
||||
to?: string,
|
||||
newTab?: boolean,
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/**
|
||||
* ForkAwesomeIcon: renders a ForkAwesome icon.
|
||||
* Full list: https://forkaweso.me/Fork-Awesome/icons/
|
||||
* @module soapbox/components/fork_awesome_icon
|
||||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
export default class ForkAwesomeIcon extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
className: PropTypes.string,
|
||||
fixedWidth: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { id, className, fixedWidth, ...other } = this.props;
|
||||
|
||||
// Use the Fork Awesome retweet icon, but change its alt
|
||||
// tag. There is a common adblocker rule which hides elements with
|
||||
// alt='retweet' unless the domain is twitter.com. This should
|
||||
// change what screenreaders call it as well.
|
||||
const alt = (id === 'retweet') ? 'repost' : id;
|
||||
|
||||
return (
|
||||
<i
|
||||
role='img'
|
||||
alt={alt}
|
||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* ForkAwesomeIcon: renders a ForkAwesome icon.
|
||||
* Full list: https://forkaweso.me/Fork-Awesome/icons/
|
||||
* @module soapbox/components/fork_awesome_icon
|
||||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||
id: string,
|
||||
className?: string,
|
||||
fixedWidth?: boolean,
|
||||
}
|
||||
|
||||
const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth, ...rest }) => {
|
||||
// Use the Fork Awesome retweet icon, but change its alt
|
||||
// tag. There is a common adblocker rule which hides elements with
|
||||
// alt='retweet' unless the domain is twitter.com. This should
|
||||
// change what screenreaders call it as well.
|
||||
// const alt = (id === 'retweet') ? 'repost' : id;
|
||||
|
||||
return (
|
||||
<i
|
||||
role='img'
|
||||
// alt={alt}
|
||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForkAwesomeIcon;
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* Icon: abstract icon class that can render icons from multiple sets.
|
||||
* @module soapbox/components/icon
|
||||
* @see soapbox/components/fork_awesome_icon
|
||||
* @see soapbox/components/svg_icon
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import ForkAwesomeIcon from './fork_awesome_icon';
|
||||
import SvgIcon from './svg_icon';
|
||||
|
||||
export default class Icon extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
id: PropTypes.string,
|
||||
src: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
className: PropTypes.string,
|
||||
fixedWidth: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { id, src, fixedWidth, ...rest } = this.props;
|
||||
|
||||
if (src) {
|
||||
return <SvgIcon src={src} {...rest} />;
|
||||
} else {
|
||||
return <ForkAwesomeIcon id={id} fixedWidth={fixedWidth} {...rest} />;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* Icon: abstract icon class that can render icons from multiple sets.
|
||||
* @module soapbox/components/icon
|
||||
* @see soapbox/components/fork_awesome_icon
|
||||
* @see soapbox/components/svg_icon
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ForkAwesomeIcon, { IForkAwesomeIcon } from './fork_awesome_icon';
|
||||
import SvgIcon, { ISvgIcon } from './svg_icon';
|
||||
|
||||
export type IIcon = IForkAwesomeIcon | ISvgIcon;
|
||||
|
||||
const Icon: React.FC<IIcon> = (props) => {
|
||||
if ((props as ISvgIcon).src) {
|
||||
const { src, ...rest } = (props as ISvgIcon);
|
||||
|
||||
return <SvgIcon src={src} {...rest} />;
|
||||
} else {
|
||||
const { id, fixedWidth, ...rest } = (props as IForkAwesomeIcon);
|
||||
|
||||
return <ForkAwesomeIcon id={id} fixedWidth={fixedWidth} {...rest} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default Icon;
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import Icon, { IIcon } from 'soapbox/components/icon';
|
||||
import { Counter } from 'soapbox/components/ui';
|
||||
|
||||
interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
@ -12,7 +12,7 @@ interface IIconWithCounter extends React.HTMLAttributes<HTMLDivElement> {
|
|||
const IconWithCounter: React.FC<IIconWithCounter> = ({ icon, count, ...rest }) => {
|
||||
return (
|
||||
<div className='relative'>
|
||||
<Icon id={icon} {...rest} />
|
||||
<Icon id={icon} {...rest as IIcon} />
|
||||
|
||||
{count > 0 && (
|
||||
<i className='absolute -top-2 -right-2'>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import DropdownMenu from 'soapbox/containers/dropdown_menu_container';
|
||||
|
@ -11,8 +11,20 @@ import SidebarNavigationLink from './sidebar-navigation-link';
|
|||
|
||||
import type { Menu } from 'soapbox/components/dropdown_menu';
|
||||
|
||||
const messages = defineMessages({
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
bookmarks: { id: 'column.bookmarks', defaultMessage: 'Bookmarks' },
|
||||
lists: { id: 'column.lists', defaultMessage: 'Lists' },
|
||||
developers: { id: 'navigation.developers', defaultMessage: 'Developers' },
|
||||
dashboard: { id: 'tabs_bar.dashboard', defaultMessage: 'Dashboard' },
|
||||
all: { id: 'tabs_bar.all', defaultMessage: 'All' },
|
||||
fediverse: { id: 'tabs_bar.fediverse', defaultMessage: 'Fediverse' },
|
||||
});
|
||||
|
||||
/** Desktop sidebar with links to different views in the app. */
|
||||
const SidebarNavigation = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const instance = useAppSelector((state) => state.instance);
|
||||
const settings = useAppSelector((state) => getSettings(state));
|
||||
const account = useOwnAccount();
|
||||
|
@ -30,7 +42,7 @@ const SidebarNavigation = () => {
|
|||
if (account.locked || followRequestsCount > 0) {
|
||||
menu.push({
|
||||
to: '/follow_requests',
|
||||
text: <FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' />,
|
||||
text: intl.formatMessage(messages.follow_requests),
|
||||
icon: require('@tabler/icons/user-plus.svg'),
|
||||
count: followRequestsCount,
|
||||
});
|
||||
|
@ -39,7 +51,7 @@ const SidebarNavigation = () => {
|
|||
if (features.bookmarks) {
|
||||
menu.push({
|
||||
to: '/bookmarks',
|
||||
text: <FormattedMessage id='column.bookmarks' defaultMessage='Bookmarks' />,
|
||||
text: intl.formatMessage(messages.bookmarks),
|
||||
icon: require('@tabler/icons/bookmark.svg'),
|
||||
});
|
||||
}
|
||||
|
@ -47,7 +59,7 @@ const SidebarNavigation = () => {
|
|||
if (features.lists) {
|
||||
menu.push({
|
||||
to: '/lists',
|
||||
text: <FormattedMessage id='column.lists' defaultMessage='Lists' />,
|
||||
text: intl.formatMessage(messages.lists),
|
||||
icon: require('@tabler/icons/list.svg'),
|
||||
});
|
||||
}
|
||||
|
@ -56,7 +68,7 @@ const SidebarNavigation = () => {
|
|||
menu.push({
|
||||
to: '/developers',
|
||||
icon: require('@tabler/icons/code.svg'),
|
||||
text: <FormattedMessage id='navigation.developers' defaultMessage='Developers' />,
|
||||
text: intl.formatMessage(messages.developers),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -64,7 +76,7 @@ const SidebarNavigation = () => {
|
|||
menu.push({
|
||||
to: '/soapbox/admin',
|
||||
icon: require('@tabler/icons/dashboard.svg'),
|
||||
text: <FormattedMessage id='tabs_bar.dashboard' defaultMessage='Dashboard' />,
|
||||
text: intl.formatMessage(messages.dashboard),
|
||||
count: dashboardCount,
|
||||
});
|
||||
}
|
||||
|
@ -78,7 +90,7 @@ const SidebarNavigation = () => {
|
|||
menu.push({
|
||||
to: '/timeline/local',
|
||||
icon: features.federating ? require('@tabler/icons/users.svg') : require('@tabler/icons/world.svg'),
|
||||
text: features.federating ? instance.title : <FormattedMessage id='tabs_bar.all' defaultMessage='All' />,
|
||||
text: features.federating ? instance.title : intl.formatMessage(messages.all),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -86,7 +98,7 @@ const SidebarNavigation = () => {
|
|||
menu.push({
|
||||
to: '/timeline/fediverse',
|
||||
icon: require('icons/fediverse.svg'),
|
||||
text: <FormattedMessage id='tabs_bar.fediverse' defaultMessage='Fediverse' />,
|
||||
text: intl.formatMessage(messages.fediverse),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ interface IStatusList extends Omit<IScrollableList, 'onLoadMore' | 'children'> {
|
|||
/** ID of the timeline in Redux. */
|
||||
timelineId?: string,
|
||||
/** Whether to display a gap or border between statuses in the list. */
|
||||
divideType: 'space' | 'border',
|
||||
divideType?: 'space' | 'border',
|
||||
}
|
||||
|
||||
/** Feed of statuses, built atop ScrollableList. */
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
import throttle from 'lodash/throttle';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { injectIntl, defineMessages } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
|
||||
import { CardHeader, CardTitle } from './ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { settings: Settings }) => {
|
||||
return {
|
||||
onOpenSettings() {
|
||||
dispatch(openModal('COMPONENT', { component: Settings }));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(undefined, mapDispatchToProps)
|
||||
@injectIntl
|
||||
@withRouter
|
||||
class SubNavigation extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
intl: PropTypes.object.isRequired,
|
||||
message: PropTypes.string,
|
||||
settings: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
|
||||
onOpenSettings: PropTypes.func.isRequired,
|
||||
history: PropTypes.object,
|
||||
}
|
||||
|
||||
state = {
|
||||
scrolled: false,
|
||||
}
|
||||
|
||||
handleBackClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
this.props.history.push('/');
|
||||
} else {
|
||||
this.props.history.goBack();
|
||||
}
|
||||
}
|
||||
|
||||
handleBackKeyUp = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.handleClick();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.attachScrollListener();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.detachScrollListener();
|
||||
}
|
||||
|
||||
attachScrollListener() {
|
||||
window.addEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
detachScrollListener() {
|
||||
window.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
|
||||
handleScroll = throttle(() => {
|
||||
if (this.node) {
|
||||
const { offsetTop } = this.node;
|
||||
|
||||
if (offsetTop > 0) {
|
||||
this.setState({ scrolled: true });
|
||||
} else {
|
||||
this.setState({ scrolled: false });
|
||||
}
|
||||
}
|
||||
}, 150, { trailing: true });
|
||||
|
||||
handleOpenSettings = () => {
|
||||
this.props.onOpenSettings();
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, message } = this.props;
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
onBackClick={this.handleBackClick}
|
||||
>
|
||||
<CardTitle title={message} />
|
||||
</CardHeader>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
// import throttle from 'lodash/throttle';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
// import { connect } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
// import { openModal } from 'soapbox/actions/modals';
|
||||
// import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
import { CardHeader, CardTitle } from './ui';
|
||||
|
||||
const messages = defineMessages({
|
||||
back: { id: 'column_back_button.label', defaultMessage: 'Back' },
|
||||
settings: { id: 'column_header.show_settings', defaultMessage: 'Show settings' },
|
||||
});
|
||||
|
||||
interface ISubNavigation {
|
||||
message: String,
|
||||
settings?: React.ComponentType,
|
||||
}
|
||||
|
||||
const SubNavigation: React.FC<ISubNavigation> = ({ message }) => {
|
||||
const intl = useIntl();
|
||||
// const dispatch = useAppDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
// const ref = useRef(null);
|
||||
|
||||
// const [scrolled, setScrolled] = useState(false);
|
||||
|
||||
// const onOpenSettings = () => {
|
||||
// dispatch(openModal('COMPONENT', { component: Settings }));
|
||||
// };
|
||||
|
||||
const handleBackClick = () => {
|
||||
if (window.history && window.history.length === 1) {
|
||||
history.push('/');
|
||||
} else {
|
||||
history.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
// const handleBackKeyUp = (e) => {
|
||||
// if (e.key === 'Enter') {
|
||||
// handleClick();
|
||||
// }
|
||||
// }
|
||||
|
||||
// const handleOpenSettings = () => {
|
||||
// onOpenSettings();
|
||||
// }
|
||||
|
||||
// useEffect(() => {
|
||||
// const handleScroll = throttle(() => {
|
||||
// if (this.node) {
|
||||
// const { offsetTop } = this.node;
|
||||
|
||||
// if (offsetTop > 0) {
|
||||
// setScrolled(true);
|
||||
// } else {
|
||||
// setScrolled(false);
|
||||
// }
|
||||
// }
|
||||
// }, 150, { trailing: true });
|
||||
|
||||
// window.addEventListener('scroll', handleScroll);
|
||||
|
||||
// return () => {
|
||||
// window.removeEventListener('scroll', handleScroll);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<CardHeader
|
||||
aria-label={intl.formatMessage(messages.back)}
|
||||
onBackClick={handleBackClick}
|
||||
>
|
||||
<CardTitle title={message} />
|
||||
</CardHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubNavigation;
|
|
@ -1,33 +0,0 @@
|
|||
/**
|
||||
* SvgIcon: abstact component to render SVG icons.
|
||||
* @module soapbox/components/svg_icon
|
||||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
export default class SvgIcon extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
src: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
|
||||
alt: PropTypes.string,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { src, className, alt, ...other } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('svg-icon', className)}
|
||||
{...other}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* SvgIcon: abstact component to render SVG icons.
|
||||
* @module soapbox/components/svg_icon
|
||||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
export interface ISvgIcon extends React.HTMLAttributes<HTMLDivElement> {
|
||||
src: string,
|
||||
id?: string,
|
||||
alt?: string,
|
||||
className?: string,
|
||||
}
|
||||
|
||||
const SvgIcon: React.FC<ISvgIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('svg-icon', className)}
|
||||
{...rest}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SvgIcon;
|
|
@ -84,7 +84,7 @@ const Aliases = () => {
|
|||
<Text tag='span'>{alias}</Text>
|
||||
</div>
|
||||
<div className='flex items-center' role='button' tabIndex={0} onClick={handleFilterDelete} data-value={alias} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='pr-1.5 text-lg' id='times' size={40} />
|
||||
<Icon className='pr-1.5 text-lg' id='times' />
|
||||
<Text weight='bold' theme='muted'><FormattedMessage id='aliases.aliases_list_delete' defaultMessage='Unlink alias' /></Text>
|
||||
</div>
|
||||
</HStack>
|
||||
|
|
|
@ -216,7 +216,7 @@ const Filters = () => {
|
|||
</div>
|
||||
</div>
|
||||
<div className='filter__delete' role='button' tabIndex={0} onClick={handleFilterDelete} data-value={filter.id} aria-label={intl.formatMessage(messages.delete)}>
|
||||
<Icon className='filter__delete-icon' id='times' size={40} />
|
||||
<Icon className='filter__delete-icon' id='times' />
|
||||
<span className='filter__delete-label'><FormattedMessage id='filters.filters_list_delete' defaultMessage='Delete' /></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { fetchPinnedStatuses } from 'soapbox/actions/pin_statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import StatusList from 'soapbox/components/status_list';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned posts' },
|
||||
});
|
||||
|
||||
const mapStateToProps = (state, { params }) => {
|
||||
const username = params.username || '';
|
||||
const me = state.get('me');
|
||||
const meUsername = state.getIn(['accounts', me, 'username'], '');
|
||||
return {
|
||||
isMyAccount: (username.toLowerCase() === meUsername.toLowerCase()),
|
||||
statusIds: state.status_lists.get('pins').items,
|
||||
hasMore: !!state.status_lists.get('pins').next,
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@injectIntl
|
||||
class PinnedStatuses extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
statusIds: ImmutablePropTypes.orderedSet.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
hasMore: PropTypes.bool.isRequired,
|
||||
isMyAccount: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatch(fetchPinnedStatuses());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl, statusIds, hasMore, isMyAccount } = this.props;
|
||||
|
||||
if (!isMyAccount) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
hasMore={hasMore}
|
||||
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { fetchPinnedStatuses } from 'soapbox/actions/pin_statuses';
|
||||
import MissingIndicator from 'soapbox/components/missing_indicator';
|
||||
import StatusList from 'soapbox/components/status_list';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Column from '../ui/components/column';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.pins', defaultMessage: 'Pinned posts' },
|
||||
});
|
||||
|
||||
const PinnedStatuses = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
const { username } = useParams<{ username: string }>();
|
||||
|
||||
const meUsername = useAppSelector((state) => state.accounts.get(state.me)?.username || '');
|
||||
const statusIds = useAppSelector((state) => state.status_lists.get('pins')!.items);
|
||||
const isLoading = useAppSelector((state) => !!state.status_lists.get('pins')!.isLoading);
|
||||
const hasMore = useAppSelector((state) => !!state.status_lists.get('pins')!.next);
|
||||
|
||||
const isMyAccount = username.toLowerCase() === meUsername.toLowerCase();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchPinnedStatuses());
|
||||
}, []);
|
||||
|
||||
if (!isMyAccount) {
|
||||
return (
|
||||
<MissingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<StatusList
|
||||
statusIds={statusIds}
|
||||
scrollKey='pinned_statuses'
|
||||
hasMore={hasMore}
|
||||
isLoading={isLoading}
|
||||
emptyMessage={<FormattedMessage id='pinned_statuses.none' defaultMessage='No pins to show.' />}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default PinnedStatuses;
|
|
@ -1,63 +0,0 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
copyright: soapboxConfig.get('copyright'),
|
||||
navlinks: soapboxConfig.getIn(['navlinks', 'homeFooter'], ImmutableList()),
|
||||
locale: getSettings(state).get('locale'),
|
||||
};
|
||||
};
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Footer extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
copyright: PropTypes.string,
|
||||
locale: PropTypes.string,
|
||||
navlinks: ImmutablePropTypes.list,
|
||||
}
|
||||
|
||||
render() {
|
||||
const { copyright, locale, navlinks } = this.props;
|
||||
|
||||
return (
|
||||
<footer className='relative max-w-7xl mt-auto mx-auto py-12 px-4 sm:px-6 xl:flex xl:items-center xl:justify-between lg:px-8'>
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
{navlinks.map((link, idx) => {
|
||||
const url = link.get('url');
|
||||
const isExternal = url.startsWith('http');
|
||||
const Comp = isExternal ? 'a' : Link;
|
||||
const compProps = isExternal ? { href: url, target: '_blank' } : { to: url };
|
||||
|
||||
return (
|
||||
<div key={idx} className='px-5 py-2'>
|
||||
<Comp {...compProps} className='hover:underline'>
|
||||
<Text tag='span' theme='primary' size='sm'>
|
||||
{link.getIn(['titleLocales', locale]) || link.get('title')}
|
||||
</Text>
|
||||
</Comp>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 xl:mt-0'>
|
||||
<Text theme='muted' align='center' size='sm'>{copyright}</Text>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { getSettings } from 'soapbox/actions/settings';
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { Text } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { FooterItem } from 'soapbox/types/soapbox';
|
||||
|
||||
const Footer = () => {
|
||||
const { copyright, navlinks, locale } = useAppSelector((state) => {
|
||||
const soapboxConfig = getSoapboxConfig(state);
|
||||
|
||||
return {
|
||||
copyright: soapboxConfig.copyright,
|
||||
navlinks: (soapboxConfig.navlinks.get('homeFooter') || ImmutableList()) as ImmutableList<FooterItem>,
|
||||
locale: getSettings(state).get('locale') as string,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className='relative max-w-7xl mt-auto mx-auto py-12 px-4 sm:px-6 xl:flex xl:items-center xl:justify-between lg:px-8'>
|
||||
<div className='flex flex-wrap justify-center'>
|
||||
{navlinks.map((link, idx) => {
|
||||
const url = link.get('url');
|
||||
const isExternal = url.startsWith('http');
|
||||
const Comp = (isExternal ? 'a' : Link) as 'a';
|
||||
const compProps = isExternal ? { href: url, target: '_blank' } : { to: url };
|
||||
|
||||
return (
|
||||
<div key={idx} className='px-5 py-2'>
|
||||
<Comp {...compProps} className='hover:underline'>
|
||||
<Text tag='span' theme='primary' size='sm'>
|
||||
{(link.getIn(['titleLocales', locale]) || link.get('title')) as string}
|
||||
</Text>
|
||||
</Comp>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className='mt-6 xl:mt-0'>
|
||||
<Text theme='muted' align='center' size='sm'>{copyright}</Text>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
|
@ -40,7 +40,7 @@ const ActionsModal: React.FC<IActionsModal> = ({ status, actions, onClick, onClo
|
|||
className={classNames({ active, destructive })}
|
||||
data-method={isLogout ? 'delete' : null}
|
||||
>
|
||||
{icon && <Icon title={text} src={icon} role='presentation' tabIndex='-1' inverted />}
|
||||
{icon && <Icon title={text} src={icon} role='presentation' tabIndex={-1} />}
|
||||
<div>
|
||||
<div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div>
|
||||
<div>{meta}</div>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import IconButton from '../../../components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this page.' },
|
||||
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
|
||||
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
class BundleModalError extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
onRetry: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
}
|
||||
|
||||
handleRetry = () => {
|
||||
this.props.onRetry();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onClose, intl: { formatMessage } } = this.props;
|
||||
|
||||
// Keep the markup in sync with <ModalLoading />
|
||||
// (make sure they have the same dimensions)
|
||||
return (
|
||||
<div className='modal-root__modal error-modal'>
|
||||
<div className='error-modal__body'>
|
||||
<IconButton title={formatMessage(messages.retry)} icon='refresh' onClick={this.handleRetry} size={64} />
|
||||
{formatMessage(messages.error)}
|
||||
</div>
|
||||
|
||||
<div className='error-modal__footer'>
|
||||
<div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className='error-modal__nav onboarding-modal__skip'
|
||||
>
|
||||
{formatMessage(messages.close)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default injectIntl(BundleModalError);
|
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import IconButton from 'soapbox/components/icon_button';
|
||||
|
||||
const messages = defineMessages({
|
||||
error: { id: 'bundle_modal_error.message', defaultMessage: 'Something went wrong while loading this page.' },
|
||||
retry: { id: 'bundle_modal_error.retry', defaultMessage: 'Try again' },
|
||||
close: { id: 'bundle_modal_error.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
interface IBundleModalError {
|
||||
onRetry: () => void,
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
const BundleModalError: React.FC<IBundleModalError> = ({ onRetry, onClose }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const handleRetry = () => {
|
||||
onRetry();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='modal-root__modal error-modal'>
|
||||
<div className='error-modal__body'>
|
||||
<IconButton title={intl.formatMessage(messages.retry)} icon='refresh' onClick={handleRetry} size={64} />
|
||||
{intl.formatMessage(messages.error)}
|
||||
</div>
|
||||
|
||||
<div className='error-modal__footer'>
|
||||
<div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className='error-modal__nav onboarding-modal__skip'
|
||||
>
|
||||
{intl.formatMessage(messages.close)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BundleModalError;
|
Loading…
Reference in New Issue