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 1/4] 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} | Bin 8591 -> 9083 bytes app/soapbox/components/dropdown_menu.tsx | 2 +- app/soapbox/components/fork_awesome_icon.js | Bin 1058 -> 0 bytes app/soapbox/components/fork_awesome_icon.tsx | 34 +++++++ app/soapbox/components/icon.js | Bin 825 -> 0 bytes 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 | Bin 2366 -> 0 bytes app/soapbox/components/sub_navigation.tsx | 83 ++++++++++++++++++ app/soapbox/components/svg_icon.js | Bin 809 -> 0 bytes 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 | Bin 2096 -> 0 bytes .../public_layout/components/footer.tsx | 51 +++++++++++ .../features/ui/components/actions_modal.tsx | 2 +- .../ui/components/bundle_modal_error.js | Bin 1559 -> 0 bytes .../ui/components/bundle_modal_error.tsx | 45 ++++++++++ 19 files changed, 295 insertions(+), 14 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 9a7ff45dc94c35ddffed4617bf17022f794a7502..69d29261f4a4fc1bbcec02e0c1ce870939ba145e 100644 GIT binary patch delta 1353 zcma)6O-~a+7|tSCT_TEziWqqzA+*T`!NkOrkA{yD2#_>@XEW_i%gAixM7ywuN=Jg=JEw84e)$X1yvlw@EEp zkU?BhiXF-~u?rb>Y(R)bv2ci!SeAhejl?=a*yAQGGEU7D%*;YXFw@YL(KXbQi~#`Z z@XBB|g z!wWIB1aDxiqzOK6c@7$)6)p(moVzrz%2|@CuB&^UPnM_KdTSQVh1F%lYH4W0U_)b= z38hx=FMoX)F>yf&{N2PiZ88eFFnww_aa;ZByQX%pe$i?tDlj8tnLsL#rL5I^7H3L0oL& OSrTH8RrFR(>3;yR-`0Bo delta 861 zcmaiy&ubGw6vx?C+hk)cRufYz^aa7Ru*UW#EfHgX9eN3-UcBgJ_azxSnOS#cYeSJB z=vm90MZ9{GJqY5>i~odwf*>MZyg1vqW>bakWq01^JKy=v+xKhp#oEhyu^tG2r{|-D zRv@}i_Rad5>iLK3%b5fnJ}t)Iri$_%nXs<>(N?aLPzf2f+bGq2?23o9ylUI7;8N*Y zX0`xTSdc1kz$F2yx{y>+!K10M@76T#`yx=_VT*)Jfqp%4V+9Uu8$c?es0(qDL9(i; zgL(D{3vmVhMy?od%G-{Cx^}AA9|A_Dipu~Vm88kgOK)om!3NZ4#Kn$a9tLT_&=)}P z#^8b`c>@``YSgS1a<@X|9ek7vX@K6qSii~6mC}|&Sz1-f&h<}J3BJ}LyghRB|Jn(@ zjlH}6al=8PBe*Ue^JBR0CcI9hI{A?Jmv0I;lxd$~M+N@}##xgyZnJQt)2;Ao0?pWX z$mM8v6XPd(e)4lv$b7M~@lfF+RB!59{*vCZKUx?~FxUU*!$}rkG|J=$i#Oru?WD4o87aSw?1Kc~3>Rb6A ndZ)0g-xoe*9iVXnIMBqNk3L*{WsR4Y%F(^qx06w>v_1A4=q@1% diff --git a/app/soapbox/components/dropdown_menu.tsx b/app/soapbox/components/dropdown_menu.tsx index 957583b602..b074122703 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 1d85f1288167583354760e95bcd70d92b541157f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1058 zcmZuwO^=&E488L!e5gbcC2-G{eyCEVy;Q5!Rz2=c7!yW2Fm!B0D#U-UXBJ4REr(>V zpWpL5n|!xRUA7-&Nvy<0#J-gQrZx)syO`+-B?ig`p;^337GSyBmJqKL*y^>!A!_2pjL z1-!6$Mi3dYhu-72<#sS82>`KTQ$kRE>!Of2$Yb;o*4{E=Z`yNa=n;SS#-rXx(0ar7 z*f&6BE~tMQ&Gjwf=JF%0mAX z*#Pn!eiBL?qa83$Ty%IqYTxo&`T!ln(RX(Rw{k6 zu6z|$AGT$vO<6;2v`h{yHep|2FkCM*!dRrUW(s|4ktjB$U877JxLNRwJ`;tYC=se% zZ}mbk_czpvfl80q%JkzGP%2q~|TQ2r?jZtvu8N|Gu03zO+q+W-In diff --git a/app/soapbox/components/fork_awesome_icon.tsx b/app/soapbox/components/fork_awesome_icon.tsx new file mode 100644 index 0000000000..616a3959d6 --- /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 3a7059061460eed8728e3b87d6539d23043f504e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 825 zcmah{O;3a{6uk2*-fcgEaNiZ9F){I?8#VD@%%;F&w+fW>l@&wy@4j|HP&bi7Lp#&y z%(UTh>A@0i6IHH2#ALKc3`m|rk_#d*dto3EC1@;D)D9xZv?`#eb0e!9fzVK3WiAVq z)^28wtc_><{sj2ZA{$#UmAgmf~6xb*wX?w!AU z*D~-Y3MziX#Jnz!8t%gHM1fPK=^>eHFnyM(*-sgMy zM;}HFAA%sz$Z!V{zcWdJO-xRPvt}^o9fOcnHnlF{d?=m4sO6!9wioIkyjZ(BI>4N; c_b_h#eZ?}3TA6keQ&Bq_JSl90dS2&!0r?vhS^xk5 diff --git a/app/soapbox/components/icon.tsx b/app/soapbox/components/icon.tsx new file mode 100644 index 0000000000..cba7b58050 --- /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 d0fd093a66..2d95cb9f9f 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 f4cf64e49f..427b0ea306 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 f75ca802fdb70f6956367f68ea1786283b300454..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2366 zcmbVNOK;ma5Wf3Y%&C$AM{W;$Nvj~(G{7!)^FZuk4~rt8CCcJPBDExy2MquB&hR10 zcF;qO9F*ZV!*6E38H%=3+JddMvbNz+Y1Kl~sFIm_8Li3Znee!Kt-AGF$4vls`2U$D z!-#p{tgzu+)Ax9^10nz8h5b|7W&tIygyc8e7`DZT`>D^FK*-dwP*S4j5lZv2f1Sc! zgsmS`Z@E53I^$WXT7<`RcAz>gZ!p_tfE&fSje1=oaiOGHwscDAjZRlgmw#|pa=ifh zUJEK+Qg=+d%K9cQm(_5=#*02q45?3nL zH#XD?vA-iIf&Btw7;deQTXP(zb__F9tCz=!c`wuo0YeZMb3Y&Hv+k=f9kWHfR@8k1 zIizJUEs%N)eGaQY1om^tFM#sX-1b_!PXHzFu-Xxo&Vv~shD(QTq{-FY&Hdf&_3aud z@1RhqKgp#%xCLnf{#+IIpJ(_tsz4U4;7to%hG@o6Lx}05(8k!Xi zGX_@u=9WE+Eh9w$f3;ke2As~b`(E>_$PP22hFAm^K(pazB@daBrq5xRp-gRP7&2iV z`2W2?EmJ!oN@j{QR%46(@mTOlB+2h8x-9*7G*-PV7AMzzpZhR}-I!iRekY`AVXV?` zQ=F$y(e^QIj_RPkV-X6inuepUD;ys_plT+|hLe1+8c{rvng+E1hzimdAxrg=g$v=s z2RO834VPP6L!RgGX%vw~hm2}&qHEUmrcRS(5_JL`nGC9X$6>38SQ_y$vyt;({Owyu zocR%Ho;~q5M=80MxP6i%Vm^dR`6T=x?ysV}QN6UoXgG?@T2|C6SI@t(3tV#TW4N*p z-v5ZE`6k=b044uvfcN8vGKJE!vb=uA@f+?>e1CB`9Y>#@=jf{EsJ(yjVPwr)^~`@E z`1DHbNPMf(V>vl~QkB>-uS~#gLYo*T z-B&p6JjFdY2;S-*UxOeb9F9ZT{mj#i%GA{n*^>?z@aeaYv = ({ 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 04f0cd526a69e794c8439aba5a1be96e42fe951b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 809 zcmZuv%Wm5+5WMp%_9okq#Ct3m1$roQX@ex_B`6A7T01OcQsGjD3qk*$CH3+l7lE3c zot=Hi-A=#`UasfI%9;mINAjxjP}xgs4H^&Lf#ji{tJjPgr@A1)$=M4e5ikKr_A}|p+4l8+yKq|m&wKz@ z&NGGvMhzFI0mfzy$va(nth3QE34yn0 zy~dO3nyH0(mVh_Ub0dg;@~6(>_hqQP5CA-BuPX>Wj6IdmGt2>3J;2gx&U^TE3XIL$ z3I;RgoZi!p|FH6Zwz;Nf{OGj9I*oFw@%M65W(+Mu|C%uDgU@mOo9*N-T!4o@xJ=ir zrNCuU$n)I#cRrQ@O7QQR^3WbdouK49XJ8X+0~EEsPES~s%6@T0n}pYrg$cfy+4E-ruYH4M&t3;;tm3mS`{Gc@!vJzlEhPIIf3|y;8-y6dCz${V AW&i*H diff --git a/app/soapbox/components/svg_icon.tsx b/app/soapbox/components/svg_icon.tsx new file mode 100644 index 0000000000..a81979d0d3 --- /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 0e765339ee..9dfb52c44e 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 fcdf262ebc..e2d6f20502 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 11a56806b88711ea57f91eab6c735a2422765af3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2096 zcmZ`)U5nc|6n)RHxNmYGPIk+-TRib>p>&~8N@)lBP)gAxH;Hy^$w=~yQ&0Z;-YeOT zou*-kM$-K}=UhdVm9ZXL_@JBz>EL}?)?VHh7!FRbv!(>0`crW;Q_`yl7rZ+kl(89)O zjZ|OXWXx=FTQ_5~4@y5=+L+p-O}C~b!yb?p_UON$_e$^G;NXm`?oG3z=9STIo6R+D)buP_@~U^Zr>D>W7n?_>7U&5ZtL*n}zA;!f6u zhc^TODpNl6WF4eH+(r zdr-172xr5BW|@PWA~kx8Hkl83nOhcrN74dfro})6$u9?gP;MDy5eObAP6B5D>s_D$ z0wNz}iMg;?P_vInI&0FW^zWvC(xTz5`h{Rv%la&?p0bqoH!|NElX2wkIoMRa)=9Dt}1C!TajE z8^2@ZF!KHfgcOj`)2tt^CE_V((s__4ZY>-v?lMlk07_1Cmx1_kUr7BRh~13%_a@u} z%YWQMH_1KC{Z#<^0gri07@fwD6C*EyczrQ}H~OtZQ|)rO9BQ5T9?T=!tY(1{uy`c` zQ5;@4FZa1{mDGZiiLAEDs#4mc0H=P^)|FSiiRhlP@R)ez>8CBc5RM*{DE;qqczzyH z8;r#KGy$gTXvO861vucd;#M!u*DKQJ^l&1X( { + 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 e123149b68..5d7b313aa3 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 70d8265e352f99e319f772ae90463c2fcbb814cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1559 zcmaJ>J#X7E5Z(PNZj%hy(o%Fttqch|1Z|2S&2G@rIc6=9DoG`A1^xHlk(4bPNemAn z`R?Prd!%eyZ5%up-98;!w6Hb0fuyDN((y*}Z6*We5k=|Zx#92AQ4bYvrNVo(Ry?C! zfK-35ba%?tFic~XlC~&tOnO&pb<;VgRYYZ39{#1KvI>>6xg^kf<53(+E9;<%wV=2t zp7t{UG)9{%=s{LjkZd|t)wphSCF-?5%3^r3;Aqj+F7{67-?~A!lj<2>sXV+I&7V72 zBh*?{d_cz*TB4dIFf6E^L1zx1)$@S&sy&$lh-V>H%v9Fe;%AvW;Jr+@9O<(RbC#(V z*20aae$ULmvnGIV4pn8rk28Dd4Bn1PLq8)b)(Iy|U<(9G6toZY)9Nv>Jh~;@cB;&z zeZ;?=G+40`jPspbom}-kj;$~S=?#14BHh3(?F7%ySEq5Ob11H3Pdtc>TeVqiFD32( zKFknNGz1U*2jBE#e{522pogtCjTj9;ejTXDDKSaOtnwUwAhvXX& void, + onClose: () => void, +} + +const BundleModalError: React.FC = ({ onRetry, onClose }) => { + const intl = useIntl(); + + const handleRetry = () => { + onRetry(); + }; + + return ( +
+
+ + {intl.formatMessage(messages.error)} +
+ +
+
+ +
+
+
+ ); +}; + +export default BundleModalError; From 37d97eb85777dd243351d000fac9eae2d7ed7f29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Mon, 11 Jul 2022 23:27:43 +0200 Subject: [PATCH 2/4] Notifications: TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/actions/notifications.ts | 4 +- app/soapbox/components/ui/column/column.tsx | 2 + .../components/clear_column_button.js | Bin 582 -> 0 bytes .../components/clear_column_button.tsx | 18 ++ .../notifications/components/filter_bar.js | Bin 3716 -> 0 bytes .../notifications/components/filter_bar.tsx | 102 ++++++++++ .../notifications/components/notification.tsx | 75 ++++--- .../containers/filter_bar_container.js | Bin 857 -> 0 bytes .../containers/notification_container.js | Bin 1814 -> 0 bytes app/soapbox/features/notifications/index.js | Bin 7441 -> 0 bytes app/soapbox/features/notifications/index.tsx | 189 ++++++++++++++++++ 11 files changed, 365 insertions(+), 25 deletions(-) delete mode 100644 app/soapbox/features/notifications/components/clear_column_button.js create mode 100644 app/soapbox/features/notifications/components/clear_column_button.tsx delete mode 100644 app/soapbox/features/notifications/components/filter_bar.js create mode 100644 app/soapbox/features/notifications/components/filter_bar.tsx delete mode 100644 app/soapbox/features/notifications/containers/filter_bar_container.js delete mode 100644 app/soapbox/features/notifications/containers/notification_container.js delete mode 100644 app/soapbox/features/notifications/index.js create mode 100644 app/soapbox/features/notifications/index.tsx diff --git a/app/soapbox/actions/notifications.ts b/app/soapbox/actions/notifications.ts index adeb46bf9c..64311a15a8 100644 --- a/app/soapbox/actions/notifications.ts +++ b/app/soapbox/actions/notifications.ts @@ -174,9 +174,9 @@ const excludeTypesFromFilter = (filter: string) => { return allTypes.filterNot(item => item === filter).toJS(); }; -const noOp = () => {}; +const noOp = () => new Promise(f => f(undefined)); -const expandNotifications = ({ maxId }: Record = {}, done = noOp) => +const expandNotifications = ({ maxId }: Record = {}, done: () => any = noOp) => (dispatch: AppDispatch, getState: () => RootState) => { if (!isLoggedIn(getState)) return dispatch(noOp); diff --git a/app/soapbox/components/ui/column/column.tsx b/app/soapbox/components/ui/column/column.tsx index 5ac4f350e0..81b7fca1e7 100644 --- a/app/soapbox/components/ui/column/column.tsx +++ b/app/soapbox/components/ui/column/column.tsx @@ -18,6 +18,8 @@ export interface IColumn { withHeader?: boolean, /** Extra class name for top
element. */ className?: string, + /** Ref forwarded to column. */ + ref?: React.Ref } /** A backdrop for the main section of the UI. */ diff --git a/app/soapbox/features/notifications/components/clear_column_button.js b/app/soapbox/features/notifications/components/clear_column_button.js deleted file mode 100644 index 709deab787278d34372d40385db7e7066e58beea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 582 zcmZXR!A`?45Jd0!ioLm&NWz&m6`~SS51^{zUOCRDwMgu+yFo=&{vF3j1&Ev+?|A%X zY}?M0z@B{f_SGRnO}+)yh5uB>4Yf+_q&c8AGS6vT|BSBi?5WivSiK;#J|PUhm8o^o z1o_QsXS|!|>~&ZAOKyC)#i0|ZBWzJH+}1#dXeCR7eQAs zM80g_6K-!_a7_{b7A@Amm}$ybgnbB~ciYCA_lKYOS=~F6Sw7%LZwafrpc?`T0EIh{ z7R&i4q4eZn5q$vG<-`&hUv-OxlE9Ug;=n}M^o%+f9FL45)}6+Y0@39z_+AQiuWmTS zRnC^P5tp+WaoDV5XH2HJ(ge3qkKuTO6bs`VNi$N$pC`2(Am70H-=o;7P|l03tqGNQ j$C(-HR4{iuKVG3?%kW34g5^4&a&BVmZQrE diff --git a/app/soapbox/features/notifications/components/clear_column_button.tsx b/app/soapbox/features/notifications/components/clear_column_button.tsx new file mode 100644 index 0000000000..3d70545aaa --- /dev/null +++ b/app/soapbox/features/notifications/components/clear_column_button.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Icon from 'soapbox/components/icon'; + +interface IClearColumnButton { + onClick: React.MouseEventHandler; +} + +const ClearColumnButton: React.FC = ({ onClick }) => ( + +); + +export default ClearColumnButton; diff --git a/app/soapbox/features/notifications/components/filter_bar.js b/app/soapbox/features/notifications/components/filter_bar.js deleted file mode 100644 index a656ce290d5400814294dc902ea9b652c1afff87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3716 zcmcInO>Y}F5WVYHOiz*kX?0hkAZ^e92I|5v(o<0s+}&A=33s_AxvB#}|GhK(khV7M zZPnD5*yPOHdGql&k!`PygFB=9PhWeq&=}pqyr;j{jt}NIgM9 zvcp@nRy?3xf$TnG2~~;PolJE zyo~)I8Je7|gEqzjyC=adjxzwE)E15)>n+SX?PMb>;Uqa0jZ_XzK?d^`dx}B1$ct>h zr(-y+2%)72+2M7VWR=6ZjR+C25eGdO>2Lz_VwVH`Nq(WCg%C@vt(!m#HaVnw?AbkH z^;)S3C~uNOx+7mOXepBkuAw9S=LBFh%0Ye%{;3xFFqO5IO>#)L{vsp{Tm4!7?wfle zw6SFl^+!e`&NL>8vUS1@b}G(fl>_~yuLZS7n3o=PuMocK0YaX9E>24t0DSaA9h(>4 zUxsm3DPb-AJnHz5zTbZk2Jq2gS6lFdUfc}^KcrEFITIiIGqM`5mIb$Qb! z>vlHHRPJ(@a>{zhwIgoSYMG%~zSd~dq+6})wQZ%sE9*IaNG7?H;M000wj(E+!b~Dw zFwA6y6(K)mIr0q(w@0()#msJMXFq#RBFvHEI+c>sZZKYCTRu346%6ejj7RI zW!AkK=*_)KRpX{PNh@wzBfHpw@!~(zdU3!?BeAK3y;f)Z{yU00-;HdQDVjfi%FXXC pyuJA4=dJ(0nST>^NB_^sF9`>{p&{HI!!DGz8GiOym { + const intl = useIntl(); + const dispatch = useAppDispatch(); + const settings = useSettings(); + const features = useFeatures(); + + const selectedFilter = settings.getIn(['notifications', 'quickFilter', 'active']) as string; + const advancedMode = settings.getIn(['notifications', 'quickFilter', 'advanced']); + + const onClick = (notificationType: string) => () => dispatch(setFilter(notificationType)); + + const items: Item[] = [ + { + text: intl.formatMessage(messages.all), + action: onClick('all'), + name: 'all', + }, + ]; + + if (!advancedMode) { + items.push({ + text: intl.formatMessage(messages.mentions), + action: onClick('mention'), + name: 'mention', + }); + } else { + items.push({ + text: , + title: intl.formatMessage(messages.mentions), + action: onClick('mention'), + name: 'mention', + }); + items.push({ + text: , + title: intl.formatMessage(messages.favourites), + action: onClick('favourite'), + name: 'favourite', + }); + if (features.emojiReacts) items.push({ + text: , + title: intl.formatMessage(messages.emoji_reacts), + action: onClick('pleroma:emoji_reaction'), + name: 'pleroma:emoji_reaction', + }); + items.push({ + text: , + title: intl.formatMessage(messages.boosts), + action: onClick('reblog'), + name: 'reblog', + }); + items.push({ + text: , + title: intl.formatMessage(messages.polls), + action: onClick('poll'), + name: 'poll', + }); + items.push({ + text: , + title: intl.formatMessage(messages.statuses), + action: onClick('status'), + name: 'status', + }); + items.push({ + text: , + title: intl.formatMessage(messages.follows), + action: onClick('follow'), + name: 'follow', + }); + items.push({ + text: , + title: intl.formatMessage(messages.moves), + action: onClick('move'), + name: 'move', + }); + } + + return ; +}; + +export default NotificationFilterBar; diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index 9f2c02778a..e87b514ecf 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -1,19 +1,27 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { HotKeys } from 'react-hotkeys'; import { defineMessages, useIntl, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; import { useHistory } from 'react-router-dom'; +import { mentionCompose } from 'soapbox/actions/compose'; +import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/interactions'; +import { openModal } from 'soapbox/actions/modals'; +import { getSettings } from 'soapbox/actions/settings'; +import { hideStatus, revealStatus } from 'soapbox/actions/statuses'; import Icon from 'soapbox/components/icon'; import Permalink from 'soapbox/components/permalink'; import { HStack, Text, Emoji } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import StatusContainer from 'soapbox/containers/status_container'; -import { useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { makeGetNotification } from 'soapbox/selectors'; import { NotificationType, validType } from 'soapbox/utils/notification'; import type { ScrollPosition } from 'soapbox/components/status'; import type { Account, Status, Notification as NotificationEntity } from 'soapbox/types/entities'; +const getNotification = makeGetNotification(); + const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => { const output = [message]; @@ -130,17 +138,17 @@ interface INotificaton { notification: NotificationEntity, onMoveUp?: (notificationId: string) => void, onMoveDown?: (notificationId: string) => void, - onMention?: (account: Account) => void, - onFavourite?: (status: Status) => void, onReblog?: (status: Status, e?: KeyboardEvent) => void, - onToggleHidden?: (status: Status) => void, getScrollPosition?: () => ScrollPosition | undefined, updateScrollBottom?: (bottom: number) => void, - siteTitle?: string, } const Notification: React.FC = (props) => { - const { hidden = false, notification, onMoveUp, onMoveDown } = props; + const { hidden = false, onMoveUp, onMoveDown } = props; + + const dispatch = useAppDispatch(); + + const notification = useAppSelector((state) => getNotification(state, props.notification)); const history = useHistory(); const intl = useIntl(); @@ -175,31 +183,52 @@ const Notification: React.FC = (props) => { } }; - const handleMention = (e?: KeyboardEvent) => { + const handleMention = useCallback((e?: KeyboardEvent) => { e?.preventDefault(); - if (props.onMention && account && typeof account === 'object') { - props.onMention(account); + if (account && typeof account === 'object') { + dispatch(mentionCompose(account)); } - }; + }, [account]); - const handleHotkeyFavourite = (e?: KeyboardEvent) => { - if (props.onFavourite && status && typeof status === 'object') { - props.onFavourite(status); + const handleHotkeyFavourite = useCallback((e?: KeyboardEvent) => { + if (status && typeof status === 'object') { + if (status.favourited) { + dispatch(unfavourite(status)); + } else { + dispatch(favourite(status)); + } } - }; + }, [status]); - const handleHotkeyBoost = (e?: KeyboardEvent) => { - if (props.onReblog && status && typeof status === 'object') { - props.onReblog(status, e); + const handleHotkeyBoost = useCallback((e?: KeyboardEvent) => { + if (status && typeof status === 'object') { + dispatch((_, getState) => { + const boostModal = getSettings(getState()).get('boostModal'); + if (status.reblogged) { + dispatch(unreblog(status)); + } else { + if (e?.shiftKey || !boostModal) { + dispatch(reblog(status)); + } else { + dispatch(openModal('BOOST', { status, onReblog: (status: Status) => { + dispatch(reblog(status)); + } })); + } + } + }); } - }; + }, [status]); - const handleHotkeyToggleHidden = (e?: KeyboardEvent) => { - if (props.onToggleHidden && status && typeof status === 'object') { - props.onToggleHidden(status); + const handleHotkeyToggleHidden = useCallback((e?: KeyboardEvent) => { + if (status && typeof status === 'object') { + if (status.hidden) { + dispatch(revealStatus(status.id)); + } else { + dispatch(hideStatus(status.id)); + } } - }; + }, [status]); const handleMoveUp = () => { if (onMoveUp) { diff --git a/app/soapbox/features/notifications/containers/filter_bar_container.js b/app/soapbox/features/notifications/containers/filter_bar_container.js deleted file mode 100644 index cfb4345cc8474eafead7ebaa0cc91bfc87f3a1f5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 857 zcmbVLK~BRk5WMFVzQvJ3a&J{65LDt&2~lncAvf6;)5H$;rc|o(ch+&7mKH8ZxvV|2 zp4r*Ry3q#NffZUQD3En*ZFJ3I1ERny17*99r%@yW&H~P5gpS4d41;Vp_G`g{D#;hyZ?3x`S=7vPTppkA*Eb$(hOh=d`tWX7! zeUps9JPxPv2{Op{4TD!6poFBR2517Rm%&IvMPWhdT~0fDfF-~S9HtVLU4 W_$Bx+i;>c{oa;=uf1P%YntcO&w<~D? diff --git a/app/soapbox/features/notifications/containers/notification_container.js b/app/soapbox/features/notifications/containers/notification_container.js deleted file mode 100644 index e9530fb83745996d3e2d12956ff721e91f7de6cd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1814 zcmah~%dXlm5Z&_?(^VWPg*}Ka+Nuv#^j3vkRb8260IR`{V&{sA%fELfj)N0G%_5L^ z&dixJkEmm1Y6oY?jnm{XY_9;UrbXghy2j!>p`QVqrheCTxo}O7b%JWrb4|kg)AS^ zI`XNFF?)1tbWZ8LeTcH1L4+gP2UXzON!M6TqQ)1LWq5wX+_v^OU~rUw@H4va#;Kjk zCF^&?#9~RsF*O5&q62_0+H~rL*Hk1s(~(EUQHiYs4!4pOGsTVhQJc!b5=08icW|ae zycNkZ@V{(vBbp*5uK{tq^~u>{1{KHX{j*fAsdXC+=(5xX8|6xzLkpNv?5|q#h~#IZs0J7uZ)igTO8h^>eNPh-;#e_46+iSDsT(m-U8 zNSOUWDr|89ENQ>^y!i!t#;HB1o%@D=;q?{XTnWU!0o*~^>0KRJW`EuIkJRn=h)CW) zJ*_v?fakV1Y1FAcUg7zjFmOqGB`Ae1nE$;Ds`M!=z>s*C=~%yh8x92Q`yg7E!BIzG zlwXhN9ggjZY-qrZ+3!pIqKX1_Fnhp{mLM6$#qA@-8?p1^dqQz8|DkzZu8*OfG3z?P dQ~Mb!a3`D6g^!uIv+FWiWaD{GMZD|C-#^duP{#lO diff --git a/app/soapbox/features/notifications/index.js b/app/soapbox/features/notifications/index.js deleted file mode 100644 index 834bb39cb7134f18f7b6a194b4a2f7e690ede622..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7441 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 QCpw>6odsQ+Qf+(k9|c#tssI20 diff --git a/app/soapbox/features/notifications/index.tsx b/app/soapbox/features/notifications/index.tsx new file mode 100644 index 0000000000..7b0848365d --- /dev/null +++ b/app/soapbox/features/notifications/index.tsx @@ -0,0 +1,189 @@ +import classNames from 'classnames'; +import { List as ImmutableList, Map as ImmutableMap } from 'immutable'; +import debounce from 'lodash/debounce'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; +import { createSelector } from 'reselect'; + +import { + expandNotifications, + scrollTopNotifications, + dequeueNotifications, +} from 'soapbox/actions/notifications'; +import { getSettings } from 'soapbox/actions/settings'; +import ScrollTopButton from 'soapbox/components/scroll-top-button'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import { Column } from 'soapbox/components/ui'; +import PlaceholderNotification from 'soapbox/features/placeholder/components/placeholder_notification'; +import { useAppDispatch, useAppSelector, useSettings } from 'soapbox/hooks'; + +import FilterBar from './components/filter_bar'; +import Notification from './components/notification'; + +import type { VirtuosoHandle } from 'react-virtuoso'; +import type { RootState } from 'soapbox/store'; +import type { Notification as NotificationEntity } from 'soapbox/types/entities'; + +const messages = defineMessages({ + title: { id: 'column.notifications', defaultMessage: 'Notifications' }, + queue: { id: 'notifications.queue_label', defaultMessage: 'Click to see {count} new {count, plural, one {notification} other {notifications}}' }, +}); + +const getNotifications = createSelector([ + state => getSettings(state).getIn(['notifications', 'quickFilter', 'show']), + state => getSettings(state).getIn(['notifications', 'quickFilter', 'active']), + state => ImmutableList((getSettings(state).getIn(['notifications', 'shows']) as ImmutableMap).filter(item => !item).keys()), + (state: RootState) => state.notifications.items.toList(), +], (showFilterBar, allowedType, excludedTypes, notifications: ImmutableList) => { + if (!showFilterBar || allowedType === 'all') { + // used if user changed the notification settings after loading the notifications from the server + // otherwise a list of notifications will come pre-filtered from the backend + // we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category + return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type'))); + } + return notifications.filter(item => item !== null && allowedType === item.get('type')); +}); + +const Notifications = () => { + const dispatch = useAppDispatch(); + const intl = useIntl(); + const settings = useSettings(); + + const showFilterBar = settings.getIn(['notifications', 'quickFilter', 'show']); + const activeFilter = settings.getIn(['notifications', 'quickFilter', 'active']); + const notifications = useAppSelector(state => getNotifications(state)); + const isLoading = useAppSelector(state => state.notifications.isLoading); + // const isUnread = useAppSelector(state => state.notifications.unread > 0); + const hasMore = useAppSelector(state => state.notifications.hasMore); + const totalQueuedNotificationsCount = useAppSelector(state => state.notifications.totalQueuedNotificationsCount || 0); + + const node = useRef(null); + const column = useRef(null); + const scrollableContentRef = useRef | null>(null); + + // const handleLoadGap = (maxId) => { + // dispatch(expandNotifications({ maxId })); + // }; + + const handleLoadOlder = useCallback(debounce(() => { + const last = notifications.last(); + dispatch(expandNotifications({ maxId: last && last.get('id') })); + }, 300, { leading: true }), []); + + const handleScrollToTop = useCallback(debounce(() => { + dispatch(scrollTopNotifications(true)); + }, 100), []); + + const handleScroll = useCallback(debounce(() => { + dispatch(scrollTopNotifications(false)); + }, 100), []); + + const handleMoveUp = (id: string) => { + const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) - 1; + _selectChild(elementIndex); + }; + + const handleMoveDown = (id: string) => { + const elementIndex = notifications.findIndex(item => item !== null && item.get('id') === id) + 1; + _selectChild(elementIndex); + }; + + const _selectChild = (index: number) => { + node.current?.scrollIntoView({ + index, + behavior: 'smooth', + done: () => { + const container = column.current; + const element = container?.querySelector(`[data-index="${index}"] .focusable`); + + if (element) { + (element as HTMLDivElement).focus(); + } + }, + }); + }; + + const handleDequeueNotifications = () => { + dispatch(dequeueNotifications()); + }; + + const handleRefresh = () => { + return dispatch(expandNotifications()); + }; + + useEffect(() => { + handleDequeueNotifications(); + dispatch(scrollTopNotifications(true)); + + return () => { + handleLoadOlder.cancel(); + handleScrollToTop.cancel(); + handleScroll.cancel(); + dispatch(scrollTopNotifications(false)); + }; + }, []); + + const emptyMessage = activeFilter === 'all' + ? + : ; + + let scrollableContent: ImmutableList | null = null; + + const filterBarContainer = showFilterBar + ? () + : null; + + if (isLoading && scrollableContentRef.current) { + scrollableContent = scrollableContentRef.current; + } else if (notifications.size > 0 || hasMore) { + scrollableContent = notifications.map((item) => ( + + )); + } else { + scrollableContent = null; + } + + scrollableContentRef.current = scrollableContent; + + const scrollContainer = ( + 0, + 'space-y-2': notifications.size === 0, + })} + > + {scrollableContent as ImmutableList} + + ); + + return ( + + {filterBarContainer} + + {scrollContainer} + + ); +}; + +export default Notifications; From d527425f671219d340248deeacc63e5e7d418ceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Tue, 12 Jul 2022 00:25:07 +0200 Subject: [PATCH 3/4] Update notification.test.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- .../components/__tests__/notification.test.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx index eacca0f99d..9c86474b7b 100644 --- a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx +++ b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; -import { updateNotifications } from '../../../../actions/notifications'; -import { render, screen, rootState, createTestStore } from '../../../../jest/test-helpers'; -import { makeGetNotification } from '../../../../selectors'; -import Notification from '../notification'; +import { updateNotifications } from 'soapbox/actions/notifications'; +import { render, screen, rootState, createTestStore } from 'soapbox/jest/test-helpers'; -const getNotification = makeGetNotification(); +import Notification from '../notification'; /** Prepare the notification for use by the component */ const normalize = (notification: any) => { @@ -15,7 +13,7 @@ const normalize = (notification: any) => { return { // @ts-ignore - notification: getNotification(state, state.notifications.items.get(notification.id)), + notification: state.notifications.items.get(notification.id), state, }; }; From 3e6dc0cfe7a79e104ebadf4e4125d8a62befc36d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Wed, 13 Jul 2022 21:03:45 +0200 Subject: [PATCH 4/4] PinnedStatuses: TS, FC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/components/status_list.tsx | 2 +- app/soapbox/features/pinned_statuses/index.js | Bin 1977 -> 0 bytes .../features/pinned_statuses/index.tsx | 51 ++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) delete mode 100644 app/soapbox/features/pinned_statuses/index.js create mode 100644 app/soapbox/features/pinned_statuses/index.tsx diff --git a/app/soapbox/components/status_list.tsx b/app/soapbox/components/status_list.tsx index 35edc9283e..4085069972 100644 --- a/app/soapbox/components/status_list.tsx +++ b/app/soapbox/components/status_list.tsx @@ -36,7 +36,7 @@ interface IStatusList extends Omit { /** 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. */ diff --git a/app/soapbox/features/pinned_statuses/index.js b/app/soapbox/features/pinned_statuses/index.js deleted file mode 100644 index 9901131ba720d15ea864590ed3b8922dc1261eb1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1977 zcmaJ?!EW0y488j+*pnT=QSND7Z0Il`SThvub{d94v6)7LEqRiRw(#=rqb$jGlXXB2 zj`jFRK9SF&X_dBcr&asywM7Gatr|#M`oFa9MS7VCkGV%)+HkJj^K`Unn$GfFji)>V zZYkpN((#_qPO+WF8%4P!$~SrD@q}5H5i}LhLxp=Gaf` z7OQX!$9Wq|L7+KRsidUX6O_iPdm&hqqlZ1(@^B{zTs>HBJ45nCW}|q!Q!gu$NhoPn zt&oo4eSKFXJ9edfJ zRmPeWj(KuiOo@2wdctqYjgz5#nX4aj;%>R-4avNK;u>6DUmIz!C2x?h9>K;pfBry9 zO+@5?8R*y(+AM7_rOeam_laplZDjT%eI;5N7-O z(Z{SIi)d!mt$Id%!wqKCL{Si1%=z|kkyBX!u8PiVDzhFQk@*+HT^irRAo798KqNqn&`=6IRVFTt0=55j%Hhp-D-^Y%^_$%kpdlCtK;fWOj*FyuhI zC@TZ+?026~QdgBI!MTEWpCX7?3Da`guJzb9S|JesKmHg&duY?Ubs|aBpt=zgVZT8@FI|pG38Fb4A18#u>mKgkeq*!=eh= zKb$683}btAWSWFVJu>)}pgkPS&)+=FPY$6OGRJ8)$+3!A&Y?IO-aTX&GgK_jN*?_71C(A8qXrHg8S5bhjr1rV%dH$>fjPtG#nPZWpRwel-19fbG@;W z*{&4**!FRLUTRg>U+}d^X=li{Cr_TCf=opp9_9?#(Dy48SoBwCUlviFQ8P(jwDiOR hi}b4kx3^#wn1gy|DXgxiOrwTZE3cDpJ>K~-`3v^SoU8x< diff --git a/app/soapbox/features/pinned_statuses/index.tsx b/app/soapbox/features/pinned_statuses/index.tsx new file mode 100644 index 0000000000..74d4d0f98a --- /dev/null +++ b/app/soapbox/features/pinned_statuses/index.tsx @@ -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 ( + + ); + } + + return ( + + } + /> + + ); +}; + +export default PinnedStatuses;