From c76639c42edc87eba18c8b44ad8e053c0bcc1d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sat, 12 Nov 2022 15:18:24 +0100 Subject: [PATCH] JS -> 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/actions/modals.ts | 6 +- app/soapbox/actions/notifications.ts | 17 ++-- .../components/extended_video_player.js | 70 -------------- .../components/extended_video_player.tsx | 64 +++++++++++++ app/soapbox/components/modal_root.tsx | 5 +- app/soapbox/components/sidebar-navigation.tsx | 2 +- .../__tests__/notification.test.tsx | 2 +- .../components/placeholder_media_gallery.js | 96 ------------------- .../components/placeholder_media_gallery.tsx | 93 ++++++++++++++++++ .../placeholder/{utils.js => utils.ts} | 4 +- app/soapbox/features/ui/components/bundle.tsx | 6 +- .../features/ui/components/media-modal.tsx | 3 +- .../{modal_root.js => modal_root.tsx} | 23 +++-- .../ui/containers/bundle_container.tsx | 3 +- ...{modal_container.js => modal_container.ts} | 14 +-- .../{notifications.js => notifications.ts} | 89 ++++++++++------- 16 files changed, 254 insertions(+), 243 deletions(-) delete mode 100644 app/soapbox/components/extended_video_player.js create mode 100644 app/soapbox/components/extended_video_player.tsx delete mode 100644 app/soapbox/features/placeholder/components/placeholder_media_gallery.js create mode 100644 app/soapbox/features/placeholder/components/placeholder_media_gallery.tsx rename app/soapbox/features/placeholder/{utils.js => utils.ts} (68%) rename app/soapbox/features/ui/components/{modal_root.js => modal_root.tsx} (83%) rename app/soapbox/features/ui/containers/{modal_container.js => modal_container.ts} (65%) rename app/soapbox/reducers/{notifications.js => notifications.ts} (64%) diff --git a/app/soapbox/actions/modals.ts b/app/soapbox/actions/modals.ts index 3e1a106cf..00d4fd01a 100644 --- a/app/soapbox/actions/modals.ts +++ b/app/soapbox/actions/modals.ts @@ -1,8 +1,10 @@ +import type { ModalType } from 'soapbox/features/ui/components/modal_root'; + export const MODAL_OPEN = 'MODAL_OPEN'; export const MODAL_CLOSE = 'MODAL_CLOSE'; /** Open a modal of the given type */ -export function openModal(type: string, props?: any) { +export function openModal(type: ModalType, props?: any) { return { type: MODAL_OPEN, modalType: type, @@ -11,7 +13,7 @@ export function openModal(type: string, props?: any) { } /** Close the modal */ -export function closeModal(type?: string) { +export function closeModal(type?: ModalType) { return { type: MODAL_CLOSE, modalType: type, diff --git a/app/soapbox/actions/notifications.ts b/app/soapbox/actions/notifications.ts index db8fd688f..8ef0e5715 100644 --- a/app/soapbox/actions/notifications.ts +++ b/app/soapbox/actions/notifications.ts @@ -1,6 +1,3 @@ -import { - Map as ImmutableMap, -} from 'immutable'; import IntlMessageFormat from 'intl-messageformat'; import 'intl-pluralrules'; import { defineMessages } from 'react-intl'; @@ -149,13 +146,13 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record< const dequeueNotifications = () => (dispatch: AppDispatch, getState: () => RootState) => { - const queuedNotifications = getState().notifications.get('queuedNotifications'); - const totalQueuedNotificationsCount = getState().notifications.get('totalQueuedNotificationsCount'); + const queuedNotifications = getState().notifications.queuedNotifications; + const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount; if (totalQueuedNotificationsCount === 0) { return; } else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { - queuedNotifications.forEach((block: APIEntity) => { + queuedNotifications.forEach((block) => { dispatch(updateNotifications(block.notification)); }); } else { @@ -184,7 +181,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an const notifications = state.notifications; const isLoadingMore = !!maxId; - if (notifications.get('isLoading')) { + if (notifications.isLoading) { done(); return dispatch(noOp); } @@ -207,7 +204,7 @@ const expandNotifications = ({ maxId }: Record = {}, done: () => an } } - if (!maxId && notifications.get('items').size > 0) { + if (!maxId && notifications.items.size > 0) { params.since_id = notifications.getIn(['items', 0, 'id']); } @@ -306,8 +303,8 @@ const markReadNotifications = () => if (!isLoggedIn(getState)) return; const state = getState(); - const topNotificationId: string | undefined = state.notifications.get('items').first(ImmutableMap()).get('id'); - const lastReadId: string | -1 = state.notifications.get('lastRead'); + const topNotificationId = state.notifications.items.first()?.id; + const lastReadId = state.notifications.lastRead; const v = parseVersion(state.instance.version); if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) { diff --git a/app/soapbox/components/extended_video_player.js b/app/soapbox/components/extended_video_player.js deleted file mode 100644 index 59a6b938e..000000000 --- a/app/soapbox/components/extended_video_player.js +++ /dev/null @@ -1,70 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import { isIOS } from 'soapbox/is_mobile'; - -export default class ExtendedVideoPlayer extends React.PureComponent { - - static propTypes = { - src: PropTypes.string.isRequired, - alt: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - time: PropTypes.number, - controls: PropTypes.bool.isRequired, - muted: PropTypes.bool.isRequired, - onClick: PropTypes.func, - }; - - handleLoadedData = () => { - if (this.props.time) { - this.video.currentTime = this.props.time; - } - } - - componentDidMount() { - this.video.addEventListener('loadeddata', this.handleLoadedData); - } - - componentWillUnmount() { - this.video.removeEventListener('loadeddata', this.handleLoadedData); - } - - setRef = (c) => { - this.video = c; - } - - handleClick = e => { - e.stopPropagation(); - const handler = this.props.onClick; - if (handler) handler(); - } - - render() { - const { src, muted, controls, alt } = this.props; - const conditionalAttributes = {}; - if (isIOS()) { - conditionalAttributes.playsInline = '1'; - } - - return ( -
-
- ); - } - -} diff --git a/app/soapbox/components/extended_video_player.tsx b/app/soapbox/components/extended_video_player.tsx new file mode 100644 index 000000000..99a0af01c --- /dev/null +++ b/app/soapbox/components/extended_video_player.tsx @@ -0,0 +1,64 @@ +import React, { useEffect, useRef } from 'react'; + +import { isIOS } from 'soapbox/is_mobile'; + +interface IExtendedVideoPlayer { + src: string, + alt?: string, + width?: number, + height?: number, + time?: number, + controls?: boolean, + muted?: boolean, + onClick?: () => void, +} + +const ExtendedVideoPlayer: React.FC = ({ src, alt, time, controls, muted, onClick }) => { + const video = useRef(null); + + useEffect(() => { + const handleLoadedData = () => { + if (time) { + video.current!.currentTime = time; + } + }; + + video.current?.addEventListener('loadeddata', handleLoadedData); + + return () => { + video.current?.removeEventListener('loadeddata', handleLoadedData); + }; + }, [video.current]); + + const handleClick: React.MouseEventHandler = e => { + e.stopPropagation(); + const handler = onClick; + if (handler) handler(); + }; + + const conditionalAttributes: React.VideoHTMLAttributes = {}; + if (isIOS()) { + conditionalAttributes.playsInline = true; + } + + return ( +
+
+ ); +}; + +export default ExtendedVideoPlayer; diff --git a/app/soapbox/components/modal_root.tsx b/app/soapbox/components/modal_root.tsx index f34597d5b..d17be3efc 100644 --- a/app/soapbox/components/modal_root.tsx +++ b/app/soapbox/components/modal_root.tsx @@ -9,6 +9,7 @@ import { openModal, closeModal } from 'soapbox/actions/modals'; import { useAppDispatch, useAppSelector, usePrevious } from 'soapbox/hooks'; import type { UnregisterCallback } from 'history'; +import type { ModalType } from 'soapbox/features/ui/components/modal_root'; import type { ReducerCompose } from 'soapbox/reducers/compose'; const messages = defineMessages({ @@ -26,8 +27,8 @@ export const checkComposeContent = (compose?: ReturnType) interface IModalRoot { onCancel?: () => void, - onClose: (type?: string) => void, - type: string, + onClose: (type?: ModalType) => void, + type: ModalType, } const ModalRoot: React.FC = ({ children, onCancel, onClose, type }) => { diff --git a/app/soapbox/components/sidebar-navigation.tsx b/app/soapbox/components/sidebar-navigation.tsx index 583006070..22ad8dfc9 100644 --- a/app/soapbox/components/sidebar-navigation.tsx +++ b/app/soapbox/components/sidebar-navigation.tsx @@ -28,7 +28,7 @@ const SidebarNavigation = () => { const instance = useAppSelector((state) => state.instance); const settings = useAppSelector((state) => getSettings(state)); const account = useOwnAccount(); - const notificationCount = useAppSelector((state) => state.notifications.get('unread')); + const notificationCount = useAppSelector((state) => state.notifications.unread); const chatsCount = useAppSelector((state) => state.chats.items.reduce((acc, curr) => acc + Math.min(curr.unread || 0, 1), 0)); const followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count()); const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count()); diff --git a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx index 9c86474b7..b9c5df641 100644 --- a/app/soapbox/features/notifications/components/__tests__/notification.test.tsx +++ b/app/soapbox/features/notifications/components/__tests__/notification.test.tsx @@ -13,7 +13,7 @@ const normalize = (notification: any) => { return { // @ts-ignore - notification: state.notifications.items.get(notification.id), + notification: state.notifications.items.get(notification.id)!, state, }; }; diff --git a/app/soapbox/features/placeholder/components/placeholder_media_gallery.js b/app/soapbox/features/placeholder/components/placeholder_media_gallery.js deleted file mode 100644 index c1b2dd3b9..000000000 --- a/app/soapbox/features/placeholder/components/placeholder_media_gallery.js +++ /dev/null @@ -1,96 +0,0 @@ -import { Map as ImmutableMap } from 'immutable'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -export default class PlaceholderMediaGallery extends React.Component { - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - defaultWidth: PropTypes.number, - } - - state = { - width: this.props.defaultWidth, - }; - - handleRef = (node) => { - if (node) { - this.setState({ - width: node.offsetWidth, - }); - } - } - - getSizeData = size => { - const { defaultWidth } = this.props; - const width = this.state.width || defaultWidth; - - const style = {}; - let itemsDimensions = []; - - if (size === 1) { - style.height = width * 9 / 16; - - itemsDimensions = [ - { w: '100%', h: '100%' }, - ]; - } else if (size === 2) { - style.height = width / 2; - - itemsDimensions = [ - { w: '50%', h: '100%', r: '2px' }, - { w: '50%', h: '100%', l: '2px' }, - ]; - } else if (size === 3) { - style.height = width; - - itemsDimensions = [ - { w: '50%', h: '50%', b: '2px', r: '2px' }, - { w: '50%', h: '50%', b: '2px', l: '2px' }, - { w: '100%', h: '50%', t: '2px' }, - ]; - } else if (size >= 4) { - style.height = width; - - itemsDimensions = [ - { w: '50%', h: '50%', b: '2px', r: '2px' }, - { w: '50%', h: '50%', b: '2px', l: '2px' }, - { w: '50%', h: '50%', t: '2px', r: '2px' }, - { w: '50%', h: '50%', t: '2px', l: '2px' }, - ]; - } - - return ImmutableMap({ - style, - itemsDimensions, - size, - width, - }); - } - - renderItem = (dimensions, i) => { - const width = dimensions.w; - const height = dimensions.h; - const top = dimensions.t || 'auto'; - const right = dimensions.r || 'auto'; - const bottom = dimensions.b || 'auto'; - const left = dimensions.l || 'auto'; - const float = dimensions.float || 'left'; - const position = dimensions.pos || 'relative'; - - return
; - } - - render() { - const { media } = this.props; - const sizeData = this.getSizeData(media.size); - - return ( -
- {media.take(4).map((_, i) => this.renderItem(sizeData.get('itemsDimensions')[i], i))} -
- ); - } - -} diff --git a/app/soapbox/features/placeholder/components/placeholder_media_gallery.tsx b/app/soapbox/features/placeholder/components/placeholder_media_gallery.tsx new file mode 100644 index 000000000..3eab10e4d --- /dev/null +++ b/app/soapbox/features/placeholder/components/placeholder_media_gallery.tsx @@ -0,0 +1,93 @@ +import { List as ImmutableList, Record as ImmutableRecord } from 'immutable'; +import React, { useState } from 'react'; + +import type { Attachment as AttachmentEntity } from 'soapbox/types/entities'; + +interface IPlaceholderMediaGallery { + media: ImmutableList; + defaultWidth?: number; +} + +const SizeData = ImmutableRecord({ + style: {} as React.CSSProperties, + itemsDimensions: [] as Record[], + size: 1 as number, + width: 0 as number, +}); + +const PlaceholderMediaGallery: React.FC = ({ media, defaultWidth }) => { + const [width, setWidth] = useState(defaultWidth); + + const handleRef = (node: HTMLDivElement) => { + if (node) { + setWidth(node.offsetWidth); + } + }; + + const getSizeData = (size: number) => { + const style: React.CSSProperties = {}; + let itemsDimensions: Record[] = []; + + if (size === 1) { + style.height = width! * 9 / 16; + + itemsDimensions = [ + { w: '100%', h: '100%' }, + ]; + } else if (size === 2) { + style.height = width! / 2; + + itemsDimensions = [ + { w: '50%', h: '100%', r: '2px' }, + { w: '50%', h: '100%', l: '2px' }, + ]; + } else if (size === 3) { + style.height = width; + + itemsDimensions = [ + { w: '50%', h: '50%', b: '2px', r: '2px' }, + { w: '50%', h: '50%', b: '2px', l: '2px' }, + { w: '100%', h: '50%', t: '2px' }, + ]; + } else if (size >= 4) { + style.height = width; + + itemsDimensions = [ + { w: '50%', h: '50%', b: '2px', r: '2px' }, + { w: '50%', h: '50%', b: '2px', l: '2px' }, + { w: '50%', h: '50%', t: '2px', r: '2px' }, + { w: '50%', h: '50%', t: '2px', l: '2px' }, + ]; + } + + return SizeData({ + style, + itemsDimensions, + size, + width, + }); + }; + + const renderItem = (dimensions: Record, i: number) => { + const width = dimensions.w; + const height = dimensions.h; + const top = dimensions.t || 'auto'; + const right = dimensions.r || 'auto'; + const bottom = dimensions.b || 'auto'; + const left = dimensions.l || 'auto'; + const float = dimensions.float as any || 'left'; + const position = dimensions.pos as any || 'relative'; + + return
; + }; + + const sizeData = getSizeData(media.size); + + return ( +
+ {media.take(4).map((_, i) => renderItem(sizeData.get('itemsDimensions')[i], i))} +
+ ); +}; + +export default PlaceholderMediaGallery; diff --git a/app/soapbox/features/placeholder/utils.js b/app/soapbox/features/placeholder/utils.ts similarity index 68% rename from app/soapbox/features/placeholder/utils.js rename to app/soapbox/features/placeholder/utils.ts index f2001a61d..15eb4e5e5 100644 --- a/app/soapbox/features/placeholder/utils.js +++ b/app/soapbox/features/placeholder/utils.ts @@ -1,6 +1,6 @@ export const PLACEHOLDER_CHAR = '█'; -export const generateText = length => { +export const generateText = (length: number) => { let text = ''; for (let i = 0; i < length; i++) { @@ -11,6 +11,6 @@ export const generateText = length => { }; // https://stackoverflow.com/a/7228322/8811886 -export const randomIntFromInterval = (min, max) => { +export const randomIntFromInterval = (min: number, max: number) => { return Math.floor(Math.random() * (max - min + 1) + min); }; diff --git a/app/soapbox/features/ui/components/bundle.tsx b/app/soapbox/features/ui/components/bundle.tsx index 55f6478bc..79851b978 100644 --- a/app/soapbox/features/ui/components/bundle.tsx +++ b/app/soapbox/features/ui/components/bundle.tsx @@ -3,10 +3,10 @@ import React from 'react'; const emptyComponent = () => null; const noop = () => { }; -interface BundleProps { +export interface BundleProps { fetchComponent: () => Promise, loading: React.ComponentType, - error: React.ComponentType<{ onRetry: (props: BundleProps) => void }>, + error: React.ComponentType<{ onRetry: (props?: BundleProps) => void }>, children: (mod: any) => React.ReactNode, renderDelay?: number, onFetch: () => void, @@ -57,7 +57,7 @@ class Bundle extends React.PureComponent { } } - load = (props: BundleProps) => { + load = (props?: BundleProps) => { const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props; const cachedMod = Bundle.cache.get(fetchComponent); diff --git a/app/soapbox/features/ui/components/media-modal.tsx b/app/soapbox/features/ui/components/media-modal.tsx index 92b5ecad9..1a432de3b 100644 --- a/app/soapbox/features/ui/components/media-modal.tsx +++ b/app/soapbox/features/ui/components/media-modal.tsx @@ -225,7 +225,6 @@ const MediaModal: React.FC = (props) => { muted controls={false} width={width} - link={link} height={height} key={attachment.preview_url} alt={attachment.description} @@ -298,4 +297,4 @@ const MediaModal: React.FC = (props) => { ); }; -export default MediaModal; \ No newline at end of file +export default MediaModal; diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.tsx similarity index 83% rename from app/soapbox/features/ui/components/modal_root.js rename to app/soapbox/features/ui/components/modal_root.tsx index 32425cffd..f9900d569 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.tsx @@ -35,6 +35,7 @@ import { } from 'soapbox/features/ui/util/async-components'; import BundleContainer from '../containers/bundle_container'; +import { BundleProps } from './bundle'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -71,19 +72,21 @@ const MODAL_COMPONENTS = { 'ACCOUNT_MODERATION': AccountModerationModal, }; -export default class ModalRoot extends React.PureComponent { +export type ModalType = keyof typeof MODAL_COMPONENTS | null; - static propTypes = { - type: PropTypes.string, - props: PropTypes.object, - onClose: PropTypes.func.isRequired, - }; +interface IModalRoot { + type: ModalType, + props?: Record | null, + onClose: (type?: ModalType) => void, +} + +export default class ModalRoot extends React.PureComponent { getSnapshotBeforeUpdate() { return { visible: !!this.props.type }; } - componentDidUpdate(prevProps, prevState, { visible }) { + componentDidUpdate(prevProps: IModalRoot, prevState: any, { visible }: any) { if (visible) { document.body.classList.add('with-modals'); } else { @@ -91,15 +94,15 @@ export default class ModalRoot extends React.PureComponent { } } - renderLoading = modalId => () => { + renderLoading = (modalId: string) => () => { return !['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].includes(modalId) ? : null; } - renderError = (props) => { + renderError: React.ComponentType<{ onRetry: (props?: BundleProps) => void }> = (props) => { return ; } - onClickClose = (_) => { + onClickClose = (_?: ModalType) => { const { onClose, type } = this.props; onClose(type); } diff --git a/app/soapbox/features/ui/containers/bundle_container.tsx b/app/soapbox/features/ui/containers/bundle_container.tsx index 12e4b3787..50a8b8629 100644 --- a/app/soapbox/features/ui/containers/bundle_container.tsx +++ b/app/soapbox/features/ui/containers/bundle_container.tsx @@ -1,6 +1,7 @@ import { connect } from 'react-redux'; -import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from '../../../actions/bundles'; +import { fetchBundleRequest, fetchBundleSuccess, fetchBundleFail } from 'soapbox/actions/bundles'; + import Bundle from '../components/bundle'; import type { AppDispatch } from 'soapbox/store'; diff --git a/app/soapbox/features/ui/containers/modal_container.js b/app/soapbox/features/ui/containers/modal_container.ts similarity index 65% rename from app/soapbox/features/ui/containers/modal_container.js rename to app/soapbox/features/ui/containers/modal_container.ts index 54f07a371..dc24254c3 100644 --- a/app/soapbox/features/ui/containers/modal_container.js +++ b/app/soapbox/features/ui/containers/modal_container.ts @@ -4,22 +4,24 @@ import { cancelReplyCompose } from 'soapbox/actions/compose'; import { closeModal } from 'soapbox/actions/modals'; import { cancelReport } from 'soapbox/actions/reports'; -import ModalRoot from '../components/modal_root'; +import ModalRoot, { ModalType } from '../components/modal_root'; -const mapStateToProps = state => { - const modal = state.get('modals').last({ +import type { AppDispatch, RootState } from 'soapbox/store'; + +const mapStateToProps = (state: RootState) => { + const modal = state.modals.last({ modalType: null, modalProps: {}, }); return { - type: modal.modalType, + type: modal.modalType as ModalType, props: modal.modalProps, }; }; -const mapDispatchToProps = (dispatch) => ({ - onClose(type) { +const mapDispatchToProps = (dispatch: AppDispatch) => ({ + onClose(type?: ModalType) { switch (type) { case 'COMPOSE': dispatch(cancelReplyCompose()); diff --git a/app/soapbox/reducers/notifications.js b/app/soapbox/reducers/notifications.ts similarity index 64% rename from app/soapbox/reducers/notifications.js rename to app/soapbox/reducers/notifications.ts index 4d5dd9921..d8c27292b 100644 --- a/app/soapbox/reducers/notifications.js +++ b/app/soapbox/reducers/notifications.ts @@ -1,4 +1,5 @@ import { + Map as ImmutableMap, Record as ImmutableRecord, OrderedMap as ImmutableOrderedMap, fromJS, @@ -33,40 +34,53 @@ import { } from '../actions/notifications'; import { TIMELINE_DELETE } from '../actions/timelines'; +import type { AnyAction } from 'redux'; +import type { APIEntity } from 'soapbox/types/entities'; + +const QueuedNotificationRecord = ImmutableRecord({ + notification: {} as APIEntity, + intlMessages: {} as Record, + intlLocale: '', +}); + const ReducerRecord = ImmutableRecord({ - items: ImmutableOrderedMap(), + items: ImmutableOrderedMap(), hasMore: true, top: false, unread: 0, isLoading: false, - queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS + queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS totalQueuedNotificationsCount: 0, //used for queuedItems overflow for MAX_QUEUED_NOTIFICATIONS+ - lastRead: -1, + lastRead: -1 as string | -1, }); -const parseId = id => parseInt(id, 10); +type State = ReturnType; +type NotificationRecord = ReturnType; +type QueuedNotification = ReturnType; + +const parseId = (id: string | number) => parseInt(id as string, 10); // For sorting the notifications -const comparator = (a, b) => { - const parse = m => parseId(m.get('id')); +const comparator = (a: NotificationRecord, b: NotificationRecord) => { + const parse = (m: NotificationRecord) => parseId(m.id); if (parse(a) < parse(b)) return 1; if (parse(a) > parse(b)) return -1; return 0; }; -const minifyNotification = notification => { +const minifyNotification = (notification: NotificationRecord) => { return notification.mergeWith((o, n) => n || o, { - account: notification.getIn(['account', 'id']), - target: notification.getIn(['target', 'id']), - status: notification.getIn(['status', 'id']), + account: notification.getIn(['account', 'id']) as string, + target: notification.getIn(['target', 'id']) as string, + status: notification.getIn(['status', 'id']) as string, }); }; -const fixNotification = notification => { +const fixNotification = (notification: APIEntity) => { return minifyNotification(normalizeNotification(notification)); }; -const isValid = notification => { +const isValid = (notification: APIEntity) => { try { // Ensure the notification is a known type if (!validType(notification.type)) { @@ -90,7 +104,7 @@ const isValid = notification => { }; // Count how many notifications appear after the given ID (for unread count) -const countFuture = (notifications, lastId) => { +const countFuture = (notifications: ImmutableOrderedMap, lastId: string | number) => { return notifications.reduce((acc, notification) => { if (parseId(notification.get('id')) > parseId(lastId)) { return acc + 1; @@ -100,8 +114,9 @@ const countFuture = (notifications, lastId) => { }, 0); }; -const importNotification = (state, notification) => { - const top = state.get('top'); + +const importNotification = (state: State, notification: APIEntity) => { + const top = state.top; if (!top) state = state.update('unread', unread => unread + 1); @@ -114,14 +129,14 @@ const importNotification = (state, notification) => { }); }; -const processRawNotifications = notifications => ( +export const processRawNotifications = (notifications: APIEntity[]) => ( ImmutableOrderedMap( notifications .filter(isValid) .map(n => [n.id, fixNotification(n)]), )); -const expandNormalizedNotifications = (state, notifications, next) => { +const expandNormalizedNotifications = (state: State, notifications: APIEntity[], next: string | null) => { const items = processRawNotifications(notifications); return state.withMutations(mutable => { @@ -132,28 +147,28 @@ const expandNormalizedNotifications = (state, notifications, next) => { }); }; -const filterNotifications = (state, relationship) => { - return state.update('items', map => map.filterNot(item => item !== null && item.get('account') === relationship.id)); +const filterNotifications = (state: State, relationship: APIEntity) => { + return state.update('items', map => map.filterNot(item => item !== null && item.account === relationship.id)); }; -const filterNotificationIds = (state, accountIds, type) => { - const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type'))); +const filterNotificationIds = (state: State, accountIds: Array, type?: string) => { + const helper = (list: ImmutableOrderedMap) => list.filterNot(item => item !== null && accountIds.includes(item.account as string) && (type === undefined || type === item.type)); return state.update('items', helper); }; -const updateTop = (state, top) => { +const updateTop = (state: State, top: boolean) => { if (top) state = state.set('unread', 0); return state.set('top', top); }; -const deleteByStatus = (state, statusId) => { - return state.update('items', map => map.filterNot(item => item !== null && item.get('status') === statusId)); +const deleteByStatus = (state: State, statusId: string) => { + return state.update('items', map => map.filterNot(item => item !== null && item.status === statusId)); }; -const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => { - const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableOrderedMap()); - const listedNotifications = state.getIn(['items'], ImmutableOrderedMap()); - const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0); +const updateNotificationsQueue = (state: State, notification: APIEntity, intlMessages: Record, intlLocale: string) => { + const queuedNotifications = state.queuedNotifications; + const listedNotifications = state.items; + const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount; const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id); if (alreadyExists) return state; @@ -162,25 +177,25 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) return state.withMutations(mutable => { if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) { - mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, { + mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, QueuedNotificationRecord({ notification, intlMessages, intlLocale, - })); + }))); } mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1); }); }; -const importMarker = (state, marker) => { - const lastReadId = marker.getIn(['notifications', 'last_read_id'], -1); +const importMarker = (state: State, marker: ImmutableMap) => { + const lastReadId = marker.getIn(['notifications', 'last_read_id'], -1) as string | -1; if (!lastReadId) { return state; } return state.withMutations(state => { - const notifications = state.get('items'); + const notifications = state.items; const unread = countFuture(notifications, lastReadId); state.set('unread', unread); @@ -188,14 +203,14 @@ const importMarker = (state, marker) => { }); }; -export default function notifications(state = ReducerRecord(), action) { +export default function notifications(state: State = ReducerRecord(), action: AnyAction) { switch (action.type) { case NOTIFICATIONS_EXPAND_REQUEST: return state.set('isLoading', true); case NOTIFICATIONS_EXPAND_FAIL: return state.set('isLoading', false); case NOTIFICATIONS_FILTER_SET: - return state.delete('items').set('hasMore', true); + return state.set('items', ImmutableOrderedMap()).set('hasMore', true); case NOTIFICATIONS_SCROLL_TOP: return updateTop(state, action.top); case NOTIFICATIONS_UPDATE: @@ -217,13 +232,13 @@ export default function notifications(state = ReducerRecord(), action) { case FOLLOW_REQUEST_REJECT_SUCCESS: return filterNotificationIds(state, [action.id], 'follow_request'); case NOTIFICATIONS_CLEAR: - return state.delete('items').set('hasMore', false); + return state.set('items', ImmutableOrderedMap()).set('hasMore', false); case NOTIFICATIONS_MARK_READ_REQUEST: return state.set('lastRead', action.lastRead); case MARKER_FETCH_SUCCESS: case MARKER_SAVE_REQUEST: case MARKER_SAVE_SUCCESS: - return importMarker(state, fromJS(action.marker)); + return importMarker(state, ImmutableMap(fromJS(action.marker))); case TIMELINE_DELETE: return deleteByStatus(state, action.id); default: