diff --git a/app/soapbox/actions/modals.ts b/app/soapbox/actions/modals.ts index 3e1a106cf9..00d4fd01a4 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 db8fd688f0..8ef0e57156 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 59a6b938e5..0000000000 Binary files a/app/soapbox/components/extended_video_player.js and /dev/null differ diff --git a/app/soapbox/components/extended_video_player.tsx b/app/soapbox/components/extended_video_player.tsx new file mode 100644 index 0000000000..99a0af01c4 --- /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 f34597d5b5..d17be3efc4 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 583006070d..22ad8dfc98 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 9c86474b7b..b9c5df6414 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 c1b2dd3b9d..0000000000 Binary files a/app/soapbox/features/placeholder/components/placeholder_media_gallery.js and /dev/null differ 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 0000000000..3eab10e4d0 --- /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 f2001a61d5..15eb4e5e52 100644 Binary files a/app/soapbox/features/placeholder/utils.js and b/app/soapbox/features/placeholder/utils.ts differ diff --git a/app/soapbox/features/ui/components/bundle.tsx b/app/soapbox/features/ui/components/bundle.tsx index 55f6478bca..79851b9789 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 92b5ecad98..1a432de3b7 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 32425cffd3..f9900d569b 100644 Binary files a/app/soapbox/features/ui/components/modal_root.js and b/app/soapbox/features/ui/components/modal_root.tsx differ diff --git a/app/soapbox/features/ui/containers/bundle_container.tsx b/app/soapbox/features/ui/containers/bundle_container.tsx index 12e4b37877..50a8b8629b 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 54f07a371f..dc24254c39 100644 Binary files a/app/soapbox/features/ui/containers/modal_container.js and b/app/soapbox/features/ui/containers/modal_container.ts differ 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 4d5dd99214..d8c27292b0 100644 Binary files a/app/soapbox/reducers/notifications.js and b/app/soapbox/reducers/notifications.ts differ