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 | Bin 2537 -> 0 bytes app/soapbox/features/chats/chat_room.tsx | 68 +++++++++++++++++++++++ 2 files changed, 68 insertions(+) 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 4d9140650e4da4b2bd2273aece7fa3c3991aa9cf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2537 zcma)7+m72d5PkPoOo0HB04W1@XjmvI(eliyn>R&}$;hZPqSDB# zP@P;-Dur$;_VVlJOIprJ8)kSQ1CjYjoqW~PEFe$$KB53*jhbqMGBpq6|6WlWXviV9 zcx&v(hPABLFeH%QLcaoVE|`MNH%I3w`#8Co3@8c$S)3HDcQXDE(V>pk*vW?1^x@D` zI^UHa*Ne7~lO#3j_Xk2n*p{2Fm6ifP&97(ZII4M+tkDM9A<{K?c4HQF=bQRPj((leqdc?;LbN+d?r=0x|{oVRa8uC*kZJMh4ac!hRZUT<#&rg=_xDH zM-)Kg<;FPo84@{$jeHH$Xr_lw7OBwp{BtL;?_I_M$0RgTdzz;UGbhYEvtz&%Gjc$) z93k9Zsmk4x<}(+mN>1kkWpQM(toTkg9gf<8`iECj!vL70(5ax?Un*YXHLs{q{KY{P zowmNNe@K3S`IO)SFmZtRyCq`cnS<{roq()g<>aSOyv%b%&xdP!(>-YQuNyE>812g?|Ig=|@@Qg)=L*uG&Mp8-KG6H+mRsK2R$Feo*?P) = ({ 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 0000000000..d910747cb3 --- /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 1c891653eb..083cbf6750 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 | Bin 1216 -> 1217 bytes app/soapbox/features/chats/chat_room.tsx | 4 +- .../features/chats/components/chat_box.js | Bin 5723 -> 0 bytes .../features/chats/components/chat_box.tsx | 192 ++++++++++++++++++ .../compose/components/upload_button.tsx | 10 +- 5 files changed, 199 insertions(+), 7 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 460c2f0790cbe9f6f11934e85aa88c418d5df40b..ce1550ba474ac8ea6a929f23ce4701bee65dfd1f 100644 GIT binary patch delta 12 TcmX@Wd608L0%PjNgaj4<9}xs! delta 10 RcmX@ed4O|5!p6h|762Fz1Nr~} diff --git a/app/soapbox/features/chats/chat_room.tsx b/app/soapbox/features/chats/chat_room.tsx index 41b108e3f2..866c16ec92 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 b4aee7a345e1ecbb399bbf1c76a25910b7f98771..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5723 zcmbtY+j84D5`FhqATL8|N2Z*4)LPkWoMgu}lbuvD`;<+oU<$IhAwdlRIZmlmHD5Cy zwqG)*8vp^)bY?GpkR_tg=stb=0ASVZ+s=xk_^I2dPHo;RE2I(EO>?mFrdG-0lh}4` zBeE)tWq&@a-1N6yyZ`yiUKww)$M17X52DRGB}?naJNh1n`qjpM*zUYv@SOaP+{U?w zPF=N#M=L#8C!G(Q^D-F40FI*4|5ByB*0!FDjoMaPy-~)UJHe}?EuJV@>WR;ND(4o zYLxp=1uzR2B$bS3jy+BW^T+-k#CH_7JLpnc^}4E+mW_G<$-!2&Sp+~rNs?0hSed|> zg3(?#uK4R3BUDBp0~7z$h}DQAIn{Sx%Tn#y8hx}xLsgrl$f$H{)Uv4b{$Pv5Bx9b* zLml`4>-S)YkYSmB|N2`dPDB%8&O?`;9tDj)=8YYkW-$Y~e{X@+&n>kYC6XKZJx9P4 z>-9#QW@7bBa7K+<3LY+QmCdt8Witwg43mLtoqym&4Wh^@s~Y;^ynXB^c-CKU%+%BY z;K}s)loI7LcukVpqVPYcFMm^-^5O8`NV_Yxb=!7%&fhz!H*JH#KZ%R`rx*B}5i07Q zAK;cDuFVww70TRi4I}oDd85Iy1SEV5Nax%FR z#26tqmDx*M?v{e;!DGc1bS^6MPW^JIpd*C1fIiteHEM52uO65=)Kf!ayGq|q8m1+Q zGTO({_B(GdH2W90w9bb_6jNX{k)=~mW$h`}Bj<9(rb9c!n&c_*^UXKOucGoep8D8Isox)N zn#%TN`5$9*$>0+$vl1UT-afjtbK@DlbX5$_OXvqNn&H zL5J=OY8BC>PBoM~hX(0~o<~kayA<_mD_nhhtROeQ^+_^wmKFtW_o3cQjGh2FX%j6T z6(Xl}NOOEjxoh>gcR_0ACa|YSM{-32Cw%%IGpe&c;)L@EDxPK!k|ZxfHuscV25yRP zMfOjPT_=`d2j%6?df9$vfmR8%*&m&P=mZo(B_LJtNqrHk)kM zA%Rg9R(7C4aVP5oPBIb30Ma8x`;Vv`jIh1MbEhYgBM>vhCGdTFr@GN<6XoI=NF``K z%}{kxV$g~EXRtFwawVPSONqVXRjnF0QnlmmU@Cd1$IvExx>(<-2GNp&^YY2+>@z_+ zO5Re$O#_Mvo5>v@twF0tr(w!9SuCVVr*mxF!nRh{oGO%gs(suUIqpYpqF98PvHUJi zija2)+M~?G%Avv!@SR*f5vC;_qaESc)TOeKRsvD2K8x3F*Sy5f&qCco@^=?A+Ev`&zR!8RASSVuTLM)QF(=?KtVki2l^WEHEVyEj)NA_6p zMCXD`q@oiI(<6m1JSUKzD*hsRE=*wd$9U1kHipbrwpObnMO|#s?D3@0_%|VZ(@a1f z0-S7?0b7sjOe|x(PC-3zB<;ecg4`OQaHi0EM4(Zgl`kFL%@c#Gv=`Frh&v)sMC(L1UcPFrj>D}E*Kl0I z2P-}vx5M_iN)Mkydp=mA Math.floor((Math.random() * 0x10000)); + +interface IChatBox { + chatId: string, + onSetInputRef: (el: HTMLTextAreaElement) => void, +} + +/** + * Chat UI with just the messages and textarea. + * Reused between floating desktop chats and fullscreen/mobile chats. + */ +const ChatBox: React.FC = ({ chatId, onSetInputRef }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const chatMessageIds = useAppSelector(state => state.chat_message_lists.get(chatId, ImmutableOrderedSet())); + + const [content, setContent] = useState(''); + const [attachment, setAttachment] = useState(undefined); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + const [resetFileKey, setResetFileKey] = useState(fileKeyGen()); + + const inputElem = useRef(null); + + const clearState = () => { + setContent(''); + setAttachment(undefined); + setIsUploading(false); + setUploadProgress(0); + setResetFileKey(fileKeyGen()); + }; + + const getParams = () => { + return { + content, + media_id: attachment && attachment.id, + }; + }; + + const canSubmit = () => { + const conds = [ + content.length > 0, + attachment, + ]; + + return conds.some(c => c); + }; + + const sendMessage = () => { + if (canSubmit() && !isUploading) { + const params = getParams(); + + dispatch(sendChatMessage(chatId, params)); + clearState(); + } + }; + + const insertLine = () => { + setContent(content + '\n'); + }; + + const handleKeyDown: React.KeyboardEventHandler = (e) => { + markRead(); + if (e.key === 'Enter' && e.shiftKey) { + insertLine(); + e.preventDefault(); + } else if (e.key === 'Enter') { + sendMessage(); + e.preventDefault(); + } + }; + + const handleContentChange: React.ChangeEventHandler = (e) => { + setContent(e.target.value); + }; + + const markRead = () => { + dispatch(markChatRead(chatId)); + }; + + const handleHover = () => { + markRead(); + }; + + const setInputRef = (el: HTMLTextAreaElement) => { + inputElem.current = el; + onSetInputRef(el); + }; + + const handleRemoveFile = () => { + setAttachment(undefined); + setResetFileKey(fileKeyGen()); + }; + + const onUploadProgress = (e: ProgressEvent) => { + const { loaded, total } = e; + setUploadProgress(loaded / total); + }; + + const handleFiles = (files: FileList) => { + setIsUploading(true); + + const data = new FormData(); + data.append('file', files[0]); + + dispatch(uploadMedia(data, onUploadProgress)).then((response: any) => { + setAttachment(response.data); + setIsUploading(false); + }).catch(() => { + setIsUploading(false); + }); + }; + + const renderAttachment = () => { + if (!attachment) return null; + + return ( +
+
+ {truncateFilename(attachment.preview_url, 20)} +
+
+ +
+
+ ); + }; + + const renderActionButton = () => { + return canSubmit() ? ( + + ) : ( + + ); + }; + + if (!chatMessageIds) return null; + + return ( +
+ + {renderAttachment()} + {isUploading && ( + + )} +
+
+ {renderActionButton()} +
+