From 1cfc16c477379d8b3964d7c0dac32e77af7144fe Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 8 Aug 2022 21:39:08 -0500 Subject: [PATCH] Status: convert to React.FC --- app/soapbox/components/status.tsx | 696 +++++++++++++----------------- 1 file changed, 300 insertions(+), 396 deletions(-) diff --git a/app/soapbox/components/status.tsx b/app/soapbox/components/status.tsx index e3dce38f5..2e2f68a1c 100644 --- a/app/soapbox/components/status.tsx +++ b/app/soapbox/components/status.tsx @@ -1,9 +1,8 @@ import classNames from 'classnames'; -import React from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { HotKeys } from 'react-hotkeys'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { injectIntl, FormattedMessage, IntlShape, defineMessages } from 'react-intl'; -import { NavLink, withRouter, RouteComponentProps } from 'react-router-dom'; +import { useIntl, FormattedMessage, IntlShape, defineMessages } from 'react-intl'; +import { NavLink, useHistory } from 'react-router-dom'; import Icon from 'soapbox/components/icon'; import AccountContainer from 'soapbox/containers/account_container'; @@ -16,7 +15,6 @@ import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; import { HStack, Text } from './ui'; -import type { History } from 'history'; import type { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import type { Account as AccountEntity, @@ -51,10 +49,9 @@ export const textForScreenReader = (intl: IntlShape, status: StatusEntity, reblo return values.join(', '); }; -interface IStatus extends RouteComponentProps { +interface IStatus { id?: string, contextType?: string, - intl: IntlShape, status: StatusEntity, account: AccountEntity, otherAccounts: ImmutableList, @@ -88,133 +85,72 @@ interface IStatus extends RouteComponentProps { displayMedia: string, allowedEmoji: ImmutableList, focusable: boolean, - history: History, featured?: boolean, withDismiss?: boolean, hideActionBar?: boolean, hoverable?: boolean, } -interface IStatusState { - showMedia: boolean, - statusId?: string, - emojiSelectorFocused: boolean, -} +const Status: React.FC = (props) => { + const { + status, + focusable = true, + hoverable = true, + onToggleHidden, + displayMedia, + onOpenMedia, + onOpenVideo, + onClick, + onReply, + onFavourite, + onReblog, + onMention, + onMoveUp, + onMoveDown, + muted, + hidden, + featured, + unread, + group, + hideActionBar, + } = props; -class Status extends ImmutablePureComponent { + const intl = useIntl(); + const history = useHistory(); - static defaultProps = { - focusable: true, - hoverable: true, + const didShowCard = useRef(false); + const node = useRef(null); + + const [showMedia, setShowMedia] = useState(defaultMediaVisibility(status, displayMedia)); + const [emojiSelectorFocused, setEmojiSelectorFocused] = useState(false); + + // Track height changes we know about to compensate scrolling. + useEffect(() => { + didShowCard.current = Boolean(!muted && !hidden && status?.card); + }, []); + + useEffect(() => { + setShowMedia(defaultMediaVisibility(status, displayMedia)); + }, [status.id]); + + const handleToggleMediaVisibility = (): void => { + setShowMedia(!showMedia); }; - didShowCard = false; - node?: HTMLDivElement = undefined; - height?: number = undefined; - - // Avoid checking props that are functions (and whose equality will always - // evaluate to false. See react-immutable-pure-component for usage. - updateOnProps: any[] = [ - 'status', - 'account', - 'muted', - 'hidden', - ]; - - state: IStatusState = { - showMedia: defaultMediaVisibility(this.props.status, this.props.displayMedia), - statusId: undefined, - emojiSelectorFocused: false, - }; - - // Track height changes we know about to compensate scrolling - componentDidMount(): void { - this.didShowCard = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card); - } - - getSnapshotBeforeUpdate(): ScrollPosition | null { - if (this.props.getScrollPosition) { - return this.props.getScrollPosition() || null; + const handleClick = (): void => { + if (onClick) { + onClick(); } else { - return null; + history.push(`/@${_properStatus().getIn(['account', 'acct'])}/posts/${_properStatus().id}`); } - } - - static getDerivedStateFromProps(nextProps: IStatus, prevState: IStatusState) { - if (nextProps.status && nextProps.status.id !== prevState.statusId) { - return { - showMedia: defaultMediaVisibility(nextProps.status, nextProps.displayMedia), - statusId: nextProps.status.id, - }; - } else { - return null; - } - } - - // Compensate height changes - componentDidUpdate(_prevProps: IStatus, _prevState: IStatusState, snapshot?: ScrollPosition): void { - const doShowCard: boolean = Boolean(!this.props.muted && !this.props.hidden && this.props.status && this.props.status.card); - - if (doShowCard && !this.didShowCard) { - this.didShowCard = true; - - if (snapshot && this.props.updateScrollBottom) { - if (this.node && this.node.offsetTop < snapshot.top) { - this.props.updateScrollBottom(snapshot.height - snapshot.top); - } - } - } - } - - componentWillUnmount(): void { - // FIXME: Run this code only when a status is being deleted. - // - // const { getScrollPosition, updateScrollBottom } = this.props; - // - // if (this.node && getScrollPosition && updateScrollBottom) { - // const position = getScrollPosition(); - // if (position && this.node.offsetTop < position.top) { - // requestAnimationFrame(() => { - // updateScrollBottom(position.height - position.top); - // }); - // } - // } - } - - handleToggleMediaVisibility = (): void => { - this.setState({ showMedia: !this.state.showMedia }); - } - - handleClick = (): void => { - if (this.props.onClick) { - this.props.onClick(); - return; - } - - if (!this.props.history) { - return; - } - - this.props.history.push(`/@${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().id}`); - } - - handleExpandClick: React.EventHandler = (e) => { - if (e.button === 0) { - if (!this.props.history) { - return; - } - - this.props.history.push(`/@${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().id}`); - } - } - - handleExpandedToggle = (): void => { - this.props.onToggleHidden(this._properStatus()); }; - handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { - const { onOpenMedia, onOpenVideo } = this.props; - const status = this._properStatus(); + const handleExpandedToggle = (): void => { + onToggleHidden(_properStatus()); + }; + + const handleHotkeyOpenMedia = (e?: KeyboardEvent): void => { + const status = _properStatus(); const firstAttachment = status.media_attachments.first(); e?.preventDefault(); @@ -226,313 +162,281 @@ class Status extends ImmutablePureComponent { onOpenMedia(status.media_attachments, 0); } } - } + }; - handleHotkeyReply = (e?: KeyboardEvent): void => { + const handleHotkeyReply = (e?: KeyboardEvent): void => { e?.preventDefault(); - this.props.onReply(this._properStatus()); - } + onReply(_properStatus()); + }; - handleHotkeyFavourite = (): void => { - this.props.onFavourite(this._properStatus()); - } + const handleHotkeyFavourite = (): void => { + onFavourite(_properStatus()); + }; - handleHotkeyBoost = (e?: KeyboardEvent): void => { - this.props.onReblog(this._properStatus(), e); - } + const handleHotkeyBoost = (e?: KeyboardEvent): void => { + onReblog(_properStatus(), e); + }; - handleHotkeyMention = (e?: KeyboardEvent): void => { + const handleHotkeyMention = (e?: KeyboardEvent): void => { e?.preventDefault(); - this.props.onMention(this._properStatus().account); - } + onMention(_properStatus().account); + }; - handleHotkeyOpen = (): void => { - this.props.history.push(`/@${this._properStatus().getIn(['account', 'acct'])}/posts/${this._properStatus().id}`); - } + const handleHotkeyOpen = (): void => { + history.push(`/@${_properStatus().getIn(['account', 'acct'])}/posts/${_properStatus().id}`); + }; - handleHotkeyOpenProfile = (): void => { - this.props.history.push(`/@${this._properStatus().getIn(['account', 'acct'])}`); - } + const handleHotkeyOpenProfile = (): void => { + history.push(`/@${_properStatus().getIn(['account', 'acct'])}`); + }; - handleHotkeyMoveUp = (e?: KeyboardEvent): void => { - this.props.onMoveUp(this.props.status.id, this.props.featured); - } + const handleHotkeyMoveUp = (e?: KeyboardEvent): void => { + onMoveUp(status.id, featured); + }; - handleHotkeyMoveDown = (e?: KeyboardEvent): void => { - this.props.onMoveDown(this.props.status.id, this.props.featured); - } + const handleHotkeyMoveDown = (e?: KeyboardEvent): void => { + onMoveDown(status.id, featured); + }; - handleHotkeyToggleHidden = (): void => { - this.props.onToggleHidden(this._properStatus()); - } + const handleHotkeyToggleHidden = (): void => { + onToggleHidden(_properStatus()); + }; - handleHotkeyToggleSensitive = (): void => { - this.handleToggleMediaVisibility(); - } + const handleHotkeyToggleSensitive = (): void => { + handleToggleMediaVisibility(); + }; - handleHotkeyReact = (): void => { - this._expandEmojiSelector(); - } + const handleHotkeyReact = (): void => { + _expandEmojiSelector(); + }; - handleEmojiSelectorExpand: React.EventHandler = e => { - if (e.key === 'Enter') { - this._expandEmojiSelector(); - } - e.preventDefault(); - } + const handleEmojiSelectorUnfocus = (): void => { + setEmojiSelectorFocused(false); + }; - handleEmojiSelectorUnfocus = (): void => { - this.setState({ emojiSelectorFocused: false }); - } - - _expandEmojiSelector = (): void => { - this.setState({ emojiSelectorFocused: true }); - const firstEmoji: HTMLDivElement | null | undefined = this.node?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); + const _expandEmojiSelector = (): void => { + setEmojiSelectorFocused(true); + const firstEmoji: HTMLDivElement | null | undefined = node.current?.querySelector('.emoji-react-selector .emoji-react-selector__emoji'); firstEmoji?.focus(); }; - _properStatus(): StatusEntity { - const { status } = this.props; - + const _properStatus = (): StatusEntity => { if (status.reblog && typeof status.reblog === 'object') { return status.reblog; } else { return status; } + }; + + if (!status) return null; + const actualStatus = _properStatus(); + let prepend, rebloggedByText, reblogElement, reblogElementMobile; + + if (hidden) { + return ( +
+ {actualStatus.getIn(['account', 'display_name']) || actualStatus.getIn(['account', 'username'])} + {actualStatus.content} +
+ ); } - handleRef = (c: HTMLDivElement): void => { - this.node = c; - } - - render() { - const poll = null; - let prepend, rebloggedByText, reblogElement, reblogElementMobile; - - const { intl, hidden, featured, unread, group } = this.props; - - // FIXME: why does this need to reassign status and account?? - let { status, account, ...other } = this.props; // eslint-disable-line prefer-const - - if (!status) return null; - - if (hidden) { - return ( -
- {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} - {status.content} -
- ); - } - - if (status.filtered || status.getIn(['reblog', 'filtered'])) { - const minHandlers = this.props.muted ? undefined : { - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - }; - - return ( - -
- -
-
- ); - } - - if (featured) { - prepend = ( -
- - - - - - - -
- ); - } - - if (status.reblog && typeof status.reblog === 'object') { - const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) }; - - reblogElement = ( - event.stopPropagation()} - className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' - > - - - - - - , - }} - /> - - - ); - - reblogElementMobile = ( -
- event.stopPropagation()} - className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' - > - - - - - - , - }} - /> - - -
- ); - - rebloggedByText = intl.formatMessage( - messages.reblogged_by, - { name: String(status.getIn(['account', 'acct'])) }, - ); - - // @ts-ignore what the FUCK - account = status.account; - status = status.reblog; - } - - let quote; - - if (status.quote) { - if (status.pleroma.get('quote_visible', true) === false) { - quote = ( -
-

-
- ); - } else { - quote = ; - } - } - - const handlers = this.props.muted ? undefined : { - reply: this.handleHotkeyReply, - favourite: this.handleHotkeyFavourite, - boost: this.handleHotkeyBoost, - mention: this.handleHotkeyMention, - open: this.handleHotkeyOpen, - openProfile: this.handleHotkeyOpenProfile, - moveUp: this.handleHotkeyMoveUp, - moveDown: this.handleHotkeyMoveDown, - toggleHidden: this.handleHotkeyToggleHidden, - toggleSensitive: this.handleHotkeyToggleSensitive, - openMedia: this.handleHotkeyOpenMedia, - react: this.handleHotkeyReact, + if (status.filtered || actualStatus.filtered) { + const minHandlers = muted ? undefined : { + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, }; - const statusUrl = `/@${status.getIn(['account', 'acct'])}/posts/${status.id}`; - // const favicon = status.getIn(['account', 'pleroma', 'favicon']); - // const domain = getDomain(status.account); - return ( - -
this.props.history.push(statusUrl)} - role='link' - > - {prepend} - -
- {reblogElementMobile} - -
- -
- -
- {!group && status.group && ( -
- Posted in {String(status.getIn(['group', 'title']))} -
- )} - - - - - - - - {poll} - {quote} - - {!this.props.hideActionBar && ( - - )} -
-
+ +
+
); } -} + if (featured) { + prepend = ( +
+ + -export default withRouter(injectIntl(Status)); + + + + +
+ ); + } + + if (status.reblog && typeof status.reblog === 'object') { + const displayNameHtml = { __html: String(status.getIn(['account', 'display_name_html'])) }; + + reblogElement = ( + event.stopPropagation()} + className='hidden sm:flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' + > + + + + + + , + }} + /> + + + ); + + reblogElementMobile = ( +
+ event.stopPropagation()} + className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-1 hover:underline' + > + + + + + + , + }} + /> + + +
+ ); + + rebloggedByText = intl.formatMessage( + messages.reblogged_by, + { name: String(status.getIn(['account', 'acct'])) }, + ); + } + + let quote; + + if (actualStatus.quote) { + if (actualStatus.pleroma.get('quote_visible', true) === false) { + quote = ( +
+

+
+ ); + } else { + quote = ; + } + } + + const handlers = muted ? undefined : { + reply: handleHotkeyReply, + favourite: handleHotkeyFavourite, + boost: handleHotkeyBoost, + mention: handleHotkeyMention, + open: handleHotkeyOpen, + openProfile: handleHotkeyOpenProfile, + moveUp: handleHotkeyMoveUp, + moveDown: handleHotkeyMoveDown, + toggleHidden: handleHotkeyToggleHidden, + toggleSensitive: handleHotkeyToggleSensitive, + openMedia: handleHotkeyOpenMedia, + react: handleHotkeyReact, + }; + + const statusUrl = `/@${actualStatus.getIn(['account', 'acct'])}/posts/${actualStatus.id}`; + + return ( + +
history.push(statusUrl)} + role='link' + > + {prepend} + +
+ {reblogElementMobile} + +
+ +
+ +
+ {!group && actualStatus.group && ( +
+ Posted in {String(actualStatus.getIn(['group', 'title']))} +
+ )} + + + + + + + + {quote} + + {!hideActionBar && ( + // @ts-ignore + + )} +
+
+
+
+ ); +}; + +export default Status;