From 9a207c970f10fdc449688699223197640f1c566c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 30 Jun 2022 16:51:36 +0200 Subject: [PATCH] TypeScript, React.FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- ...t_textarea.js => autosuggest_textarea.tsx} | 79 +++++++------ app/soapbox/components/dropdown_menu.tsx | 2 +- app/soapbox/components/fork_awesome_icon.js | 39 ------- app/soapbox/components/fork_awesome_icon.tsx | 34 ++++++ app/soapbox/components/icon.js | 33 ------ app/soapbox/components/icon.tsx | 27 +++++ app/soapbox/components/icon_with_counter.tsx | 4 +- app/soapbox/components/sidebar-navigation.tsx | 28 +++-- app/soapbox/components/sub_navigation.js | 105 ------------------ app/soapbox/components/sub_navigation.tsx | 83 ++++++++++++++ app/soapbox/components/svg_icon.js | 33 ------ app/soapbox/components/svg_icon.tsx | 29 +++++ app/soapbox/features/aliases/index.tsx | 2 +- app/soapbox/features/filters/index.tsx | 2 +- .../public_layout/components/footer.js | 63 ----------- .../public_layout/components/footer.tsx | 51 +++++++++ .../features/ui/components/actions_modal.tsx | 2 +- .../ui/components/bundle_modal_error.js | 53 --------- .../ui/components/bundle_modal_error.tsx | 45 ++++++++ 19 files changed, 337 insertions(+), 377 deletions(-) rename app/soapbox/components/{autosuggest_textarea.js => autosuggest_textarea.tsx} (79%) delete mode 100644 app/soapbox/components/fork_awesome_icon.js create mode 100644 app/soapbox/components/fork_awesome_icon.tsx delete mode 100644 app/soapbox/components/icon.js create mode 100644 app/soapbox/components/icon.tsx delete mode 100644 app/soapbox/components/sub_navigation.js create mode 100644 app/soapbox/components/sub_navigation.tsx delete mode 100644 app/soapbox/components/svg_icon.js create mode 100644 app/soapbox/components/svg_icon.tsx delete mode 100644 app/soapbox/features/public_layout/components/footer.js create mode 100644 app/soapbox/features/public_layout/components/footer.tsx delete mode 100644 app/soapbox/features/ui/components/bundle_modal_error.js create mode 100644 app/soapbox/features/ui/components/bundle_modal_error.tsx diff --git a/app/soapbox/components/autosuggest_textarea.js b/app/soapbox/components/autosuggest_textarea.tsx similarity index 79% rename from app/soapbox/components/autosuggest_textarea.js rename to app/soapbox/components/autosuggest_textarea.tsx index 9a7ff45dc..69d29261f 100644 --- a/app/soapbox/components/autosuggest_textarea.js +++ b/app/soapbox/components/autosuggest_textarea.tsx @@ -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, + disabled: boolean, + placeholder: string, + onSuggestionSelected: (tokenStart: number, token: string | null, value: string | undefined) => void, + onSuggestionsClearRequested: () => void, + onSuggestionsFetchRequested: (token: string | number) => void, + onChange: React.ChangeEventHandler, + onKeyUp: React.KeyboardEventHandler, + onKeyDown: React.KeyboardEventHandler, + 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 { + + 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 = (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 = (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 = (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 = (c) => { this.textarea = c; } - onPaste = (e) => { + onPaste: React.ClipboardEventHandler = (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 (
@@ -297,3 +300,5 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } } + +export default AutosuggestTextarea; diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index 957583b60..b07412270 100644 --- a/app/soapbox/components/dropdown_menu.tsx +++ b/app/soapbox/components/dropdown_menu.tsx @@ -18,7 +18,7 @@ let id = 0; export interface MenuItem { action?: React.EventHandler, middleClick?: React.EventHandler, - text: string | JSX.Element, + text: string, href?: string, to?: string, newTab?: boolean, diff --git a/app/soapbox/components/fork_awesome_icon.js b/app/soapbox/components/fork_awesome_icon.js deleted file mode 100644 index 1d85f1288..000000000 --- a/app/soapbox/components/fork_awesome_icon.js +++ /dev/null @@ -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 ( - - ); - } - -} diff --git a/app/soapbox/components/fork_awesome_icon.tsx b/app/soapbox/components/fork_awesome_icon.tsx new file mode 100644 index 000000000..616a3959d --- /dev/null +++ b/app/soapbox/components/fork_awesome_icon.tsx @@ -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 { + id: string, + className?: string, + fixedWidth?: boolean, +} + +const ForkAwesomeIcon: React.FC = ({ 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 ( + + ); +};`` + +export default ForkAwesomeIcon; diff --git a/app/soapbox/components/icon.js b/app/soapbox/components/icon.js deleted file mode 100644 index 3a7059061..000000000 --- a/app/soapbox/components/icon.js +++ /dev/null @@ -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 ; - } else { - return ; - } - } - -} diff --git a/app/soapbox/components/icon.tsx b/app/soapbox/components/icon.tsx new file mode 100644 index 000000000..cba7b5805 --- /dev/null +++ b/app/soapbox/components/icon.tsx @@ -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 = (props) => { + if ((props as ISvgIcon).src) { + const { src, ...rest } = (props as ISvgIcon); + + return ; + } else { + const { id, fixedWidth, ...rest } = (props as IForkAwesomeIcon); + + return ; + } +}; + +export default Icon; diff --git a/app/soapbox/components/icon_with_counter.tsx b/app/soapbox/components/icon_with_counter.tsx index d0fd093a6..2d95cb9f9 100644 --- a/app/soapbox/components/icon_with_counter.tsx +++ b/app/soapbox/components/icon_with_counter.tsx @@ -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 { @@ -12,7 +12,7 @@ interface IIconWithCounter extends React.HTMLAttributes { const IconWithCounter: React.FC = ({ icon, count, ...rest }) => { return (
- + {count > 0 && ( diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index f4cf64e49..427b0ea30 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -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: , + text: intl.formatMessage(messages.follow_requests), icon: require('@tabler/icons/icons/user-plus.svg'), count: followRequestsCount, }); @@ -39,7 +51,7 @@ const SidebarNavigation = () => { if (features.bookmarks) { menu.push({ to: '/bookmarks', - text: , + text: intl.formatMessage(messages.bookmarks), icon: require('@tabler/icons/icons/bookmark.svg'), }); } @@ -47,7 +59,7 @@ const SidebarNavigation = () => { if (features.lists) { menu.push({ to: '/lists', - text: , + text: intl.formatMessage(messages.lists), icon: require('@tabler/icons/icons/list.svg'), }); } @@ -56,7 +68,7 @@ const SidebarNavigation = () => { menu.push({ to: '/developers', icon: require('@tabler/icons/icons/code.svg'), - text: , + text: intl.formatMessage(messages.developers), }); } @@ -64,7 +76,7 @@ const SidebarNavigation = () => { menu.push({ to: '/soapbox/admin', icon: require('@tabler/icons/icons/dashboard.svg'), - text: , + text: intl.formatMessage(messages.dashboard), count: dashboardCount, }); } @@ -78,7 +90,7 @@ const SidebarNavigation = () => { menu.push({ to: '/timeline/local', icon: features.federating ? require('@tabler/icons/icons/users.svg') : require('@tabler/icons/icons/world.svg'), - text: features.federating ? instance.title : , + 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: , + text: intl.formatMessage(messages.fediverse), }); } diff --git a/app/soapbox/components/sub_navigation.js b/app/soapbox/components/sub_navigation.js deleted file mode 100644 index f75ca802f..000000000 --- a/app/soapbox/components/sub_navigation.js +++ /dev/null @@ -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 ( - - - - ); - } - -} diff --git a/app/soapbox/components/sub_navigation.tsx b/app/soapbox/components/sub_navigation.tsx new file mode 100644 index 000000000..b8e2b310d --- /dev/null +++ b/app/soapbox/components/sub_navigation.tsx @@ -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 = ({ 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 ( + + + + ); +}; + +export default SubNavigation; diff --git a/app/soapbox/components/svg_icon.js b/app/soapbox/components/svg_icon.js deleted file mode 100644 index 04f0cd526..000000000 --- a/app/soapbox/components/svg_icon.js +++ /dev/null @@ -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 ( -
- } /> -
- ); - } - -} diff --git a/app/soapbox/components/svg_icon.tsx b/app/soapbox/components/svg_icon.tsx new file mode 100644 index 000000000..a81979d0d --- /dev/null +++ b/app/soapbox/components/svg_icon.tsx @@ -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 { + src: string, + id?: string, + alt?: string, + className?: string, +} + +const SvgIcon: React.FC = ({ src, alt, className, ...rest }) => { + return ( +
+ } /> +
+ ); +}; + +export default SvgIcon; diff --git a/app/soapbox/features/aliases/index.tsx b/app/soapbox/features/aliases/index.tsx index 0e765339e..9dfb52c44 100644 --- a/app/soapbox/features/aliases/index.tsx +++ b/app/soapbox/features/aliases/index.tsx @@ -84,7 +84,7 @@ const Aliases = () => { {alias}
- +
diff --git a/app/soapbox/features/filters/index.tsx b/app/soapbox/features/filters/index.tsx index fcdf262eb..e2d6f2050 100644 --- a/app/soapbox/features/filters/index.tsx +++ b/app/soapbox/features/filters/index.tsx @@ -216,7 +216,7 @@ const Filters = () => {
- +
diff --git a/app/soapbox/features/public_layout/components/footer.js b/app/soapbox/features/public_layout/components/footer.js deleted file mode 100644 index 11a56806b..000000000 --- a/app/soapbox/features/public_layout/components/footer.js +++ /dev/null @@ -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 ( -
-
- {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 ( -
- - - {link.getIn(['titleLocales', locale]) || link.get('title')} - - -
- ); - })} -
- -
- {copyright} -
-
- ); - } - -} diff --git a/app/soapbox/features/public_layout/components/footer.tsx b/app/soapbox/features/public_layout/components/footer.tsx new file mode 100644 index 000000000..69f933339 --- /dev/null +++ b/app/soapbox/features/public_layout/components/footer.tsx @@ -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, + locale: getSettings(state).get('locale') as string, + }; + }); + + return ( +
+
+ {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 ( +
+ + + {(link.getIn(['titleLocales', locale]) || link.get('title')) as string} + + +
+ ); + })} +
+ +
+ {copyright} +
+
+ ); +}; + +export default Footer; diff --git a/app/soapbox/features/ui/components/actions_modal.tsx b/app/soapbox/features/ui/components/actions_modal.tsx index e123149b6..5d7b313aa 100644 --- a/app/soapbox/features/ui/components/actions_modal.tsx +++ b/app/soapbox/features/ui/components/actions_modal.tsx @@ -40,7 +40,7 @@ const ActionsModal: React.FC = ({ status, actions, onClick, onClo className={classNames({ active, destructive })} data-method={isLogout ? 'delete' : null} > - {icon && } + {icon && }
{text}
{meta}
diff --git a/app/soapbox/features/ui/components/bundle_modal_error.js b/app/soapbox/features/ui/components/bundle_modal_error.js deleted file mode 100644 index 70d8265e3..000000000 --- a/app/soapbox/features/ui/components/bundle_modal_error.js +++ /dev/null @@ -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 - // (make sure they have the same dimensions) - return ( -
-
- - {formatMessage(messages.error)} -
- -
-
- -
-
-
- ); - } - -} - -export default injectIntl(BundleModalError); diff --git a/app/soapbox/features/ui/components/bundle_modal_error.tsx b/app/soapbox/features/ui/components/bundle_modal_error.tsx new file mode 100644 index 000000000..2945c442b --- /dev/null +++ b/app/soapbox/features/ui/components/bundle_modal_error.tsx @@ -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 = ({ onRetry, onClose }) => { + const intl = useIntl(); + + const handleRetry = () => { + onRetry(); + }; + + return ( +
+
+ + {intl.formatMessage(messages.error)} +
+ +
+
+ +
+
+
+ ); +}; + +export default BundleModalError;