From fe9984dd9c91d3c9b301dc8e85c12a7285d0cb6b Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 Jun 2022 13:05:37 -0500 Subject: [PATCH 1/9] ChatRoom: convert to TSX --- app/soapbox/features/chats/chat_room.js | 95 ------------------------ app/soapbox/features/chats/chat_room.tsx | 68 +++++++++++++++++ 2 files changed, 68 insertions(+), 95 deletions(-) delete mode 100644 app/soapbox/features/chats/chat_room.js create mode 100644 app/soapbox/features/chats/chat_room.tsx diff --git a/app/soapbox/features/chats/chat_room.js b/app/soapbox/features/chats/chat_room.js deleted file mode 100644 index 4d9140650..000000000 --- a/app/soapbox/features/chats/chat_room.js +++ /dev/null @@ -1,95 +0,0 @@ -import { Map as ImmutableMap } 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 { injectIntl } from 'react-intl'; -import { connect } from 'react-redux'; - -import { fetchChat, markChatRead } from 'soapbox/actions/chats'; -import { Column } from 'soapbox/components/ui'; -import { makeGetChat } from 'soapbox/selectors'; -import { getAcct } from 'soapbox/utils/accounts'; -import { displayFqn } from 'soapbox/utils/state'; - -import ChatBox from './components/chat_box'; - -const mapStateToProps = (state, { params }) => { - const getChat = makeGetChat(); - const chat = state.getIn(['chats', 'items', params.chatId], ImmutableMap()).toJS(); - - return { - me: state.get('me'), - chat: getChat(state, chat), - displayFqn: displayFqn(state), - }; -}; - -export default @connect(mapStateToProps) -@injectIntl -class ChatRoom extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - chat: ImmutablePropTypes.map, - displayFqn: PropTypes.bool, - me: PropTypes.node, - } - - handleInputRef = (el) => { - this.inputElem = el; - this.focusInput(); - }; - - focusInput = () => { - if (!this.inputElem) return; - this.inputElem.focus(); - } - - markRead = () => { - const { dispatch, chat } = this.props; - if (!chat) return; - dispatch(markChatRead(chat.get('id'))); - } - - componentDidMount() { - const { dispatch, params } = this.props; - dispatch(fetchChat(params.chatId)); - this.markRead(); - } - - componentDidUpdate(prevProps) { - const markReadConditions = [ - () => this.props.chat, - () => this.props.chat.get('unread') > 0, - ]; - - if (markReadConditions.every(c => c())) - this.markRead(); - } - - render() { - const { chat, displayFqn } = this.props; - if (!chat) return null; - const account = chat.get('account'); - - return ( - - {/*
- - -
- @{getAcct(account, displayFqn)} -
- -
*/} - -
- ); - } - -} diff --git a/app/soapbox/features/chats/chat_room.tsx b/app/soapbox/features/chats/chat_room.tsx new file mode 100644 index 000000000..41b108e3f --- /dev/null +++ b/app/soapbox/features/chats/chat_room.tsx @@ -0,0 +1,68 @@ +import { Map as ImmutableMap } from 'immutable'; +import React, { useEffect, useRef } from 'react'; + +import { fetchChat, markChatRead } from 'soapbox/actions/chats'; +import { Column } from 'soapbox/components/ui'; +import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; +import { makeGetChat } from 'soapbox/selectors'; +import { getAcct } from 'soapbox/utils/accounts'; +import { displayFqn as getDisplayFqn } from 'soapbox/utils/state'; + +import ChatBox from './components/chat_box'; + +const getChat = makeGetChat(); + +interface IChatRoom { + params: { + chatId: string, + } +} + +/** Fullscreen chat UI. */ +const ChatRoom: React.FC = ({ params }) => { + const dispatch = useAppDispatch(); + const displayFqn = useAppSelector(getDisplayFqn); + const inputElem = useRef(null); + + const chat = useAppSelector(state => { + const chat = state.chats.items.get(params.chatId, ImmutableMap()).toJS() as any; + return getChat(state, chat); + }); + + const focusInput = () => { + inputElem.current?.focus(); + }; + + const handleInputRef = (el: HTMLInputElement) => { + inputElem.current = el; + focusInput(); + }; + + const markRead = () => { + if (!chat) return; + dispatch(markChatRead(chat.id)); + }; + + useEffect(() => { + dispatch(fetchChat(params.chatId)); + markRead(); + }, [params.chatId]); + + // If this component is loaded at all, we can instantly mark new messages as read. + useEffect(() => { + markRead(); + }, [chat?.unread]); + + if (!chat) return null; + + return ( + + + + ); +}; + +export default ChatRoom; From 46c1185dad42966f2aa02315e467c32bdb8dc0f2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 Jun 2022 13:42:21 -0500 Subject: [PATCH 2/9] Create generic UploadProgress component, have composer use it --- app/soapbox/components/upload-progress.tsx | 42 +++++++++++++++++++ .../compose/components/upload-progress.tsx | 35 +++------------- 2 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 app/soapbox/components/upload-progress.tsx diff --git a/app/soapbox/components/upload-progress.tsx b/app/soapbox/components/upload-progress.tsx new file mode 100644 index 000000000..d910747cb --- /dev/null +++ b/app/soapbox/components/upload-progress.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { spring } from 'react-motion'; + +import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; +import Motion from 'soapbox/features/ui/util/optional_motion'; + +interface IUploadProgress { + /** Number between 0 and 1 to represent the percentage complete. */ + progress: number, +} + +/** Displays a progress bar for uploading files. */ +const UploadProgress: React.FC = ({ progress }) => { + return ( + + + + + + + + +
+ + {({ width }) => + (
) + } + +
+ + + ); +}; + +export default UploadProgress; diff --git a/app/soapbox/features/compose/components/upload-progress.tsx b/app/soapbox/features/compose/components/upload-progress.tsx index 1c891653e..083cbf675 100644 --- a/app/soapbox/features/compose/components/upload-progress.tsx +++ b/app/soapbox/features/compose/components/upload-progress.tsx @@ -1,13 +1,10 @@ import React from 'react'; -import { FormattedMessage } from 'react-intl'; -import { spring } from 'react-motion'; -import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; +import UploadProgress from 'soapbox/components/upload-progress'; import { useAppSelector } from 'soapbox/hooks'; -import Motion from '../../ui/util/optional_motion'; - -const UploadProgress = () => { +/** File upload progress bar for post composer. */ +const ComposeUploadProgress = () => { const active = useAppSelector((state) => state.compose.get('is_uploading')); const progress = useAppSelector((state) => state.compose.get('progress')); @@ -16,30 +13,8 @@ const UploadProgress = () => { } return ( - - - - - - - - -
- - {({ width }) => - (
) - } - -
- - + ); }; -export default UploadProgress; +export default ComposeUploadProgress; From c35564c62b747f267833f547dfb0b300046656c6 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Fri, 17 Jun 2022 13:45:52 -0500 Subject: [PATCH 3/9] ChatBox: convert to TSX --- app/soapbox/actions/media.js | 2 +- app/soapbox/features/chats/chat_room.tsx | 4 +- .../features/chats/components/chat_box.js | 214 ------------------ .../features/chats/components/chat_box.tsx | 192 ++++++++++++++++ .../compose/components/upload_button.tsx | 10 +- 5 files changed, 200 insertions(+), 222 deletions(-) delete mode 100644 app/soapbox/features/chats/components/chat_box.js create mode 100644 app/soapbox/features/chats/components/chat_box.tsx diff --git a/app/soapbox/actions/media.js b/app/soapbox/actions/media.js index 460c2f079..ce1550ba4 100644 --- a/app/soapbox/actions/media.js +++ b/app/soapbox/actions/media.js @@ -2,7 +2,7 @@ import { getFeatures } from 'soapbox/utils/features'; import api from '../api'; -const noOp = () => {}; +const noOp = (e) => {}; export function fetchMedia(mediaId) { return (dispatch, getState) => { diff --git a/app/soapbox/features/chats/chat_room.tsx b/app/soapbox/features/chats/chat_room.tsx index 41b108e3f..866c16ec9 100644 --- a/app/soapbox/features/chats/chat_room.tsx +++ b/app/soapbox/features/chats/chat_room.tsx @@ -22,7 +22,7 @@ interface IChatRoom { const ChatRoom: React.FC = ({ params }) => { const dispatch = useAppDispatch(); const displayFqn = useAppSelector(getDisplayFqn); - const inputElem = useRef(null); + const inputElem = useRef(null); const chat = useAppSelector(state => { const chat = state.chats.items.get(params.chatId, ImmutableMap()).toJS() as any; @@ -33,7 +33,7 @@ const ChatRoom: React.FC = ({ params }) => { inputElem.current?.focus(); }; - const handleInputRef = (el: HTMLInputElement) => { + const handleInputRef = (el: HTMLTextAreaElement) => { inputElem.current = el; focusInput(); }; diff --git a/app/soapbox/features/chats/components/chat_box.js b/app/soapbox/features/chats/components/chat_box.js deleted file mode 100644 index b4aee7a34..000000000 --- a/app/soapbox/features/chats/components/chat_box.js +++ /dev/null @@ -1,214 +0,0 @@ -import { OrderedSet as ImmutableOrderedSet } 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 { injectIntl, defineMessages } from 'react-intl'; -import { connect } from 'react-redux'; - -import { - sendChatMessage, - markChatRead, -} from 'soapbox/actions/chats'; -import { uploadMedia } from 'soapbox/actions/media'; -import IconButton from 'soapbox/components/icon_button'; -import UploadProgress from 'soapbox/features/compose/components/upload-progress'; -import UploadButton from 'soapbox/features/compose/components/upload_button'; -import { truncateFilename } from 'soapbox/utils/media'; - -import ChatMessageList from './chat_message_list'; - -const messages = defineMessages({ - placeholder: { id: 'chat_box.input.placeholder', defaultMessage: 'Send a message…' }, - send: { id: 'chat_box.actions.send', defaultMessage: 'Send' }, -}); - -const mapStateToProps = (state, { chatId }) => ({ - me: state.get('me'), - chat: state.getIn(['chats', 'items', chatId]), - chatMessageIds: state.getIn(['chat_message_lists', chatId], ImmutableOrderedSet()), -}); - -const fileKeyGen = () => Math.floor((Math.random() * 0x10000)); - -export default @connect(mapStateToProps) -@injectIntl -class ChatBox extends ImmutablePureComponent { - - static propTypes = { - dispatch: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - chatId: PropTypes.string.isRequired, - chatMessageIds: ImmutablePropTypes.orderedSet, - chat: ImmutablePropTypes.record, - onSetInputRef: PropTypes.func, - me: PropTypes.node, - } - - initialState = () => ({ - content: '', - attachment: undefined, - isUploading: false, - uploadProgress: 0, - resetFileKey: fileKeyGen(), - }) - - state = this.initialState() - - clearState = () => { - this.setState(this.initialState()); - } - - getParams = () => { - const { content, attachment } = this.state; - - return { - content, - media_id: attachment && attachment.id, - }; - } - - canSubmit = () => { - const { content, attachment } = this.state; - - const conds = [ - content.length > 0, - attachment, - ]; - - return conds.some(c => c); - } - - sendMessage = () => { - const { dispatch, chatId } = this.props; - const { isUploading } = this.state; - - if (this.canSubmit() && !isUploading) { - const params = this.getParams(); - - dispatch(sendChatMessage(chatId, params)); - this.clearState(); - } - } - - insertLine = () => { - const { content } = this.state; - this.setState({ content: content + '\n' }); - } - - handleKeyDown = (e) => { - this.markRead(); - if (e.key === 'Enter' && e.shiftKey) { - this.insertLine(); - e.preventDefault(); - } else if (e.key === 'Enter') { - this.sendMessage(); - e.preventDefault(); - } - } - - handleContentChange = (e) => { - this.setState({ content: e.target.value }); - } - - markRead = () => { - const { dispatch, chatId } = this.props; - dispatch(markChatRead(chatId)); - } - - handleHover = () => { - this.markRead(); - } - - setInputRef = (el) => { - const { onSetInputRef } = this.props; - this.inputElem = el; - onSetInputRef(el); - }; - - handleRemoveFile = (e) => { - this.setState({ attachment: undefined, resetFileKey: fileKeyGen() }); - } - - onUploadProgress = (e) => { - const { loaded, total } = e; - this.setState({ uploadProgress: loaded / total }); - } - - handleFiles = (files) => { - const { dispatch } = this.props; - - this.setState({ isUploading: true }); - - const data = new FormData(); - data.append('file', files[0]); - - dispatch(uploadMedia(data, this.onUploadProgress)).then(response => { - this.setState({ attachment: response.data, isUploading: false }); - }).catch(() => { - this.setState({ isUploading: false }); - }); - } - - renderAttachment = () => { - const { attachment } = this.state; - if (!attachment) return null; - - return ( -
-
- {truncateFilename(attachment.preview_url, 20)} -
-
- -
-
- ); - } - - renderActionButton = () => { - const { intl } = this.props; - const { resetFileKey } = this.state; - - return this.canSubmit() ? ( - - ) : ( - - ); - } - - render() { - const { chatMessageIds, chatId, intl } = this.props; - const { content, isUploading, uploadProgress } = this.state; - if (!chatMessageIds) return null; - - return ( -
- - {this.renderAttachment()} - -
-
- {this.renderActionButton()} -
-