sforkowany z mirror/soapbox
JS -> TS, FC
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>mention-reblog-author
rodzic
3d2e7eeffe
commit
c76639c42e
|
@ -1,8 +1,10 @@
|
||||||
|
import type { ModalType } from 'soapbox/features/ui/components/modal_root';
|
||||||
|
|
||||||
export const MODAL_OPEN = 'MODAL_OPEN';
|
export const MODAL_OPEN = 'MODAL_OPEN';
|
||||||
export const MODAL_CLOSE = 'MODAL_CLOSE';
|
export const MODAL_CLOSE = 'MODAL_CLOSE';
|
||||||
|
|
||||||
/** Open a modal of the given type */
|
/** Open a modal of the given type */
|
||||||
export function openModal(type: string, props?: any) {
|
export function openModal(type: ModalType, props?: any) {
|
||||||
return {
|
return {
|
||||||
type: MODAL_OPEN,
|
type: MODAL_OPEN,
|
||||||
modalType: type,
|
modalType: type,
|
||||||
|
@ -11,7 +13,7 @@ export function openModal(type: string, props?: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Close the modal */
|
/** Close the modal */
|
||||||
export function closeModal(type?: string) {
|
export function closeModal(type?: ModalType) {
|
||||||
return {
|
return {
|
||||||
type: MODAL_CLOSE,
|
type: MODAL_CLOSE,
|
||||||
modalType: type,
|
modalType: type,
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
import {
|
|
||||||
Map as ImmutableMap,
|
|
||||||
} from 'immutable';
|
|
||||||
import IntlMessageFormat from 'intl-messageformat';
|
import IntlMessageFormat from 'intl-messageformat';
|
||||||
import 'intl-pluralrules';
|
import 'intl-pluralrules';
|
||||||
import { defineMessages } from 'react-intl';
|
import { defineMessages } from 'react-intl';
|
||||||
|
@ -149,13 +146,13 @@ const updateNotificationsQueue = (notification: APIEntity, intlMessages: Record<
|
||||||
|
|
||||||
const dequeueNotifications = () =>
|
const dequeueNotifications = () =>
|
||||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||||
const queuedNotifications = getState().notifications.get('queuedNotifications');
|
const queuedNotifications = getState().notifications.queuedNotifications;
|
||||||
const totalQueuedNotificationsCount = getState().notifications.get('totalQueuedNotificationsCount');
|
const totalQueuedNotificationsCount = getState().notifications.totalQueuedNotificationsCount;
|
||||||
|
|
||||||
if (totalQueuedNotificationsCount === 0) {
|
if (totalQueuedNotificationsCount === 0) {
|
||||||
return;
|
return;
|
||||||
} else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
} else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||||
queuedNotifications.forEach((block: APIEntity) => {
|
queuedNotifications.forEach((block) => {
|
||||||
dispatch(updateNotifications(block.notification));
|
dispatch(updateNotifications(block.notification));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@ -184,7 +181,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
const notifications = state.notifications;
|
const notifications = state.notifications;
|
||||||
const isLoadingMore = !!maxId;
|
const isLoadingMore = !!maxId;
|
||||||
|
|
||||||
if (notifications.get('isLoading')) {
|
if (notifications.isLoading) {
|
||||||
done();
|
done();
|
||||||
return dispatch(noOp);
|
return dispatch(noOp);
|
||||||
}
|
}
|
||||||
|
@ -207,7 +204,7 @@ const expandNotifications = ({ maxId }: Record<string, any> = {}, done: () => an
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!maxId && notifications.get('items').size > 0) {
|
if (!maxId && notifications.items.size > 0) {
|
||||||
params.since_id = notifications.getIn(['items', 0, 'id']);
|
params.since_id = notifications.getIn(['items', 0, 'id']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,8 +303,8 @@ const markReadNotifications = () =>
|
||||||
if (!isLoggedIn(getState)) return;
|
if (!isLoggedIn(getState)) return;
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const topNotificationId: string | undefined = state.notifications.get('items').first(ImmutableMap()).get('id');
|
const topNotificationId = state.notifications.items.first()?.id;
|
||||||
const lastReadId: string | -1 = state.notifications.get('lastRead');
|
const lastReadId = state.notifications.lastRead;
|
||||||
const v = parseVersion(state.instance.version);
|
const v = parseVersion(state.instance.version);
|
||||||
|
|
||||||
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
|
if (topNotificationId && (lastReadId === -1 || compareId(topNotificationId, lastReadId) > 0)) {
|
||||||
|
|
|
@ -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 (
|
|
||||||
<div className='extended-video-player'>
|
|
||||||
<video
|
|
||||||
ref={this.setRef}
|
|
||||||
src={src}
|
|
||||||
autoPlay
|
|
||||||
role='button'
|
|
||||||
tabIndex='0'
|
|
||||||
aria-label={alt}
|
|
||||||
title={alt}
|
|
||||||
muted={muted}
|
|
||||||
controls={controls}
|
|
||||||
loop={!controls}
|
|
||||||
onClick={this.handleClick}
|
|
||||||
{...conditionalAttributes}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<IExtendedVideoPlayer> = ({ src, alt, time, controls, muted, onClick }) => {
|
||||||
|
const video = useRef<HTMLVideoElement>(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<HTMLVideoElement> = e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const handler = onClick;
|
||||||
|
if (handler) handler();
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionalAttributes: React.VideoHTMLAttributes<HTMLVideoElement> = {};
|
||||||
|
if (isIOS()) {
|
||||||
|
conditionalAttributes.playsInline = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='extended-video-player'>
|
||||||
|
<video
|
||||||
|
ref={video}
|
||||||
|
src={src}
|
||||||
|
autoPlay
|
||||||
|
role='button'
|
||||||
|
tabIndex={0}
|
||||||
|
aria-label={alt}
|
||||||
|
title={alt}
|
||||||
|
muted={muted}
|
||||||
|
controls={controls}
|
||||||
|
loop={!controls}
|
||||||
|
onClick={handleClick}
|
||||||
|
{...conditionalAttributes}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExtendedVideoPlayer;
|
|
@ -9,6 +9,7 @@ import { openModal, closeModal } from 'soapbox/actions/modals';
|
||||||
import { useAppDispatch, useAppSelector, usePrevious } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector, usePrevious } from 'soapbox/hooks';
|
||||||
|
|
||||||
import type { UnregisterCallback } from 'history';
|
import type { UnregisterCallback } from 'history';
|
||||||
|
import type { ModalType } from 'soapbox/features/ui/components/modal_root';
|
||||||
import type { ReducerCompose } from 'soapbox/reducers/compose';
|
import type { ReducerCompose } from 'soapbox/reducers/compose';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
@ -26,8 +27,8 @@ export const checkComposeContent = (compose?: ReturnType<typeof ReducerCompose>)
|
||||||
|
|
||||||
interface IModalRoot {
|
interface IModalRoot {
|
||||||
onCancel?: () => void,
|
onCancel?: () => void,
|
||||||
onClose: (type?: string) => void,
|
onClose: (type?: ModalType) => void,
|
||||||
type: string,
|
type: ModalType,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
|
const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type }) => {
|
||||||
|
|
|
@ -28,7 +28,7 @@ const SidebarNavigation = () => {
|
||||||
const instance = useAppSelector((state) => state.instance);
|
const instance = useAppSelector((state) => state.instance);
|
||||||
const settings = useAppSelector((state) => getSettings(state));
|
const settings = useAppSelector((state) => getSettings(state));
|
||||||
const account = useOwnAccount();
|
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 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 followRequestsCount = useAppSelector((state) => state.user_lists.follow_requests.items.count());
|
||||||
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
const dashboardCount = useAppSelector((state) => state.admin.openReports.count() + state.admin.awaitingApproval.count());
|
||||||
|
|
|
@ -13,7 +13,7 @@ const normalize = (notification: any) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
notification: state.notifications.items.get(notification.id),
|
notification: state.notifications.items.get(notification.id)!,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 <div key={i} className='media-gallery__item' style={{ position, float, left, top, right, bottom, height, width }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { media } = this.props;
|
|
||||||
const sizeData = this.getSizeData(media.size);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='media-gallery media-gallery--placeholder' style={sizeData.get('style')} ref={this.handleRef}>
|
|
||||||
{media.take(4).map((_, i) => this.renderItem(sizeData.get('itemsDimensions')[i], i))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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<AttachmentEntity>;
|
||||||
|
defaultWidth?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SizeData = ImmutableRecord({
|
||||||
|
style: {} as React.CSSProperties,
|
||||||
|
itemsDimensions: [] as Record<string, string>[],
|
||||||
|
size: 1 as number,
|
||||||
|
width: 0 as number,
|
||||||
|
});
|
||||||
|
|
||||||
|
const PlaceholderMediaGallery: React.FC<IPlaceholderMediaGallery> = ({ 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<string, string>[] = [];
|
||||||
|
|
||||||
|
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<string, string>, 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 <div key={i} className='media-gallery__item' style={{ position, float, left, top, right, bottom, height, width }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeData = getSizeData(media.size);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='media-gallery media-gallery--placeholder' style={sizeData.get('style')} ref={handleRef}>
|
||||||
|
{media.take(4).map((_, i) => renderItem(sizeData.get('itemsDimensions')[i], i))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PlaceholderMediaGallery;
|
|
@ -1,6 +1,6 @@
|
||||||
export const PLACEHOLDER_CHAR = '█';
|
export const PLACEHOLDER_CHAR = '█';
|
||||||
|
|
||||||
export const generateText = length => {
|
export const generateText = (length: number) => {
|
||||||
let text = '';
|
let text = '';
|
||||||
|
|
||||||
for (let i = 0; i < length; i++) {
|
for (let i = 0; i < length; i++) {
|
||||||
|
@ -11,6 +11,6 @@ export const generateText = length => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://stackoverflow.com/a/7228322/8811886
|
// 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);
|
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||||
};
|
};
|
|
@ -3,10 +3,10 @@ import React from 'react';
|
||||||
const emptyComponent = () => null;
|
const emptyComponent = () => null;
|
||||||
const noop = () => { };
|
const noop = () => { };
|
||||||
|
|
||||||
interface BundleProps {
|
export interface BundleProps {
|
||||||
fetchComponent: () => Promise<any>,
|
fetchComponent: () => Promise<any>,
|
||||||
loading: React.ComponentType,
|
loading: React.ComponentType,
|
||||||
error: React.ComponentType<{ onRetry: (props: BundleProps) => void }>,
|
error: React.ComponentType<{ onRetry: (props?: BundleProps) => void }>,
|
||||||
children: (mod: any) => React.ReactNode,
|
children: (mod: any) => React.ReactNode,
|
||||||
renderDelay?: number,
|
renderDelay?: number,
|
||||||
onFetch: () => void,
|
onFetch: () => void,
|
||||||
|
@ -57,7 +57,7 @@ class Bundle extends React.PureComponent<BundleProps, BundleState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
load = (props: BundleProps) => {
|
load = (props?: BundleProps) => {
|
||||||
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
|
const { fetchComponent, onFetch, onFetchSuccess, onFetchFail, renderDelay } = props || this.props;
|
||||||
const cachedMod = Bundle.cache.get(fetchComponent);
|
const cachedMod = Bundle.cache.get(fetchComponent);
|
||||||
|
|
||||||
|
|
|
@ -225,7 +225,6 @@ const MediaModal: React.FC<IMediaModal> = (props) => {
|
||||||
muted
|
muted
|
||||||
controls={false}
|
controls={false}
|
||||||
width={width}
|
width={width}
|
||||||
link={link}
|
|
||||||
height={height}
|
height={height}
|
||||||
key={attachment.preview_url}
|
key={attachment.preview_url}
|
||||||
alt={attachment.description}
|
alt={attachment.description}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {
|
||||||
} from 'soapbox/features/ui/util/async-components';
|
} from 'soapbox/features/ui/util/async-components';
|
||||||
|
|
||||||
import BundleContainer from '../containers/bundle_container';
|
import BundleContainer from '../containers/bundle_container';
|
||||||
|
import { BundleProps } from './bundle';
|
||||||
|
|
||||||
import BundleModalError from './bundle_modal_error';
|
import BundleModalError from './bundle_modal_error';
|
||||||
import ModalLoading from './modal_loading';
|
import ModalLoading from './modal_loading';
|
||||||
|
@ -71,19 +72,21 @@ const MODAL_COMPONENTS = {
|
||||||
'ACCOUNT_MODERATION': AccountModerationModal,
|
'ACCOUNT_MODERATION': AccountModerationModal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class ModalRoot extends React.PureComponent {
|
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
||||||
|
|
||||||
static propTypes = {
|
interface IModalRoot {
|
||||||
type: PropTypes.string,
|
type: ModalType,
|
||||||
props: PropTypes.object,
|
props?: Record<string, any> | null,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: (type?: ModalType) => void,
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default class ModalRoot extends React.PureComponent<IModalRoot> {
|
||||||
|
|
||||||
getSnapshotBeforeUpdate() {
|
getSnapshotBeforeUpdate() {
|
||||||
return { visible: !!this.props.type };
|
return { visible: !!this.props.type };
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState, { visible }) {
|
componentDidUpdate(prevProps: IModalRoot, prevState: any, { visible }: any) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
document.body.classList.add('with-modals');
|
document.body.classList.add('with-modals');
|
||||||
} else {
|
} 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) ? <ModalLoading /> : null;
|
return !['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].includes(modalId) ? <ModalLoading /> : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError = (props) => {
|
renderError: React.ComponentType<{ onRetry: (props?: BundleProps) => void }> = (props) => {
|
||||||
return <BundleModalError {...props} onClose={this.onClickClose} />;
|
return <BundleModalError {...props} onClose={this.onClickClose} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickClose = (_) => {
|
onClickClose = (_?: ModalType) => {
|
||||||
const { onClose, type } = this.props;
|
const { onClose, type } = this.props;
|
||||||
onClose(type);
|
onClose(type);
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { connect } from 'react-redux';
|
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 Bundle from '../components/bundle';
|
||||||
|
|
||||||
import type { AppDispatch } from 'soapbox/store';
|
import type { AppDispatch } from 'soapbox/store';
|
||||||
|
|
|
@ -4,22 +4,24 @@ import { cancelReplyCompose } from 'soapbox/actions/compose';
|
||||||
import { closeModal } from 'soapbox/actions/modals';
|
import { closeModal } from 'soapbox/actions/modals';
|
||||||
import { cancelReport } from 'soapbox/actions/reports';
|
import { cancelReport } from 'soapbox/actions/reports';
|
||||||
|
|
||||||
import ModalRoot from '../components/modal_root';
|
import ModalRoot, { ModalType } from '../components/modal_root';
|
||||||
|
|
||||||
const mapStateToProps = state => {
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
const modal = state.get('modals').last({
|
|
||||||
|
const mapStateToProps = (state: RootState) => {
|
||||||
|
const modal = state.modals.last({
|
||||||
modalType: null,
|
modalType: null,
|
||||||
modalProps: {},
|
modalProps: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: modal.modalType,
|
type: modal.modalType as ModalType,
|
||||||
props: modal.modalProps,
|
props: modal.modalProps,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch) => ({
|
const mapDispatchToProps = (dispatch: AppDispatch) => ({
|
||||||
onClose(type) {
|
onClose(type?: ModalType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'COMPOSE':
|
case 'COMPOSE':
|
||||||
dispatch(cancelReplyCompose());
|
dispatch(cancelReplyCompose());
|
|
@ -1,4 +1,5 @@
|
||||||
import {
|
import {
|
||||||
|
Map as ImmutableMap,
|
||||||
Record as ImmutableRecord,
|
Record as ImmutableRecord,
|
||||||
OrderedMap as ImmutableOrderedMap,
|
OrderedMap as ImmutableOrderedMap,
|
||||||
fromJS,
|
fromJS,
|
||||||
|
@ -33,40 +34,53 @@ import {
|
||||||
} from '../actions/notifications';
|
} from '../actions/notifications';
|
||||||
import { TIMELINE_DELETE } from '../actions/timelines';
|
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<string, string>,
|
||||||
|
intlLocale: '',
|
||||||
|
});
|
||||||
|
|
||||||
const ReducerRecord = ImmutableRecord({
|
const ReducerRecord = ImmutableRecord({
|
||||||
items: ImmutableOrderedMap(),
|
items: ImmutableOrderedMap<string, NotificationRecord>(),
|
||||||
hasMore: true,
|
hasMore: true,
|
||||||
top: false,
|
top: false,
|
||||||
unread: 0,
|
unread: 0,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
queuedNotifications: ImmutableOrderedMap(), //max = MAX_QUEUED_NOTIFICATIONS
|
queuedNotifications: ImmutableOrderedMap<string, QueuedNotification>(), //max = MAX_QUEUED_NOTIFICATIONS
|
||||||
totalQueuedNotificationsCount: 0, //used for queuedItems overflow for 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<typeof ReducerRecord>;
|
||||||
|
type NotificationRecord = ReturnType<typeof normalizeNotification>;
|
||||||
|
type QueuedNotification = ReturnType<typeof QueuedNotificationRecord>;
|
||||||
|
|
||||||
|
const parseId = (id: string | number) => parseInt(id as string, 10);
|
||||||
|
|
||||||
// For sorting the notifications
|
// For sorting the notifications
|
||||||
const comparator = (a, b) => {
|
const comparator = (a: NotificationRecord, b: NotificationRecord) => {
|
||||||
const parse = m => parseId(m.get('id'));
|
const parse = (m: NotificationRecord) => parseId(m.id);
|
||||||
if (parse(a) < parse(b)) return 1;
|
if (parse(a) < parse(b)) return 1;
|
||||||
if (parse(a) > parse(b)) return -1;
|
if (parse(a) > parse(b)) return -1;
|
||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const minifyNotification = notification => {
|
const minifyNotification = (notification: NotificationRecord) => {
|
||||||
return notification.mergeWith((o, n) => n || o, {
|
return notification.mergeWith((o, n) => n || o, {
|
||||||
account: notification.getIn(['account', 'id']),
|
account: notification.getIn(['account', 'id']) as string,
|
||||||
target: notification.getIn(['target', 'id']),
|
target: notification.getIn(['target', 'id']) as string,
|
||||||
status: notification.getIn(['status', 'id']),
|
status: notification.getIn(['status', 'id']) as string,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const fixNotification = notification => {
|
const fixNotification = (notification: APIEntity) => {
|
||||||
return minifyNotification(normalizeNotification(notification));
|
return minifyNotification(normalizeNotification(notification));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValid = notification => {
|
const isValid = (notification: APIEntity) => {
|
||||||
try {
|
try {
|
||||||
// Ensure the notification is a known type
|
// Ensure the notification is a known type
|
||||||
if (!validType(notification.type)) {
|
if (!validType(notification.type)) {
|
||||||
|
@ -90,7 +104,7 @@ const isValid = notification => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Count how many notifications appear after the given ID (for unread count)
|
// Count how many notifications appear after the given ID (for unread count)
|
||||||
const countFuture = (notifications, lastId) => {
|
const countFuture = (notifications: ImmutableOrderedMap<string, NotificationRecord>, lastId: string | number) => {
|
||||||
return notifications.reduce((acc, notification) => {
|
return notifications.reduce((acc, notification) => {
|
||||||
if (parseId(notification.get('id')) > parseId(lastId)) {
|
if (parseId(notification.get('id')) > parseId(lastId)) {
|
||||||
return acc + 1;
|
return acc + 1;
|
||||||
|
@ -100,8 +114,9 @@ const countFuture = (notifications, lastId) => {
|
||||||
}, 0);
|
}, 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);
|
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(
|
ImmutableOrderedMap(
|
||||||
notifications
|
notifications
|
||||||
.filter(isValid)
|
.filter(isValid)
|
||||||
.map(n => [n.id, fixNotification(n)]),
|
.map(n => [n.id, fixNotification(n)]),
|
||||||
));
|
));
|
||||||
|
|
||||||
const expandNormalizedNotifications = (state, notifications, next) => {
|
const expandNormalizedNotifications = (state: State, notifications: APIEntity[], next: string | null) => {
|
||||||
const items = processRawNotifications(notifications);
|
const items = processRawNotifications(notifications);
|
||||||
|
|
||||||
return state.withMutations(mutable => {
|
return state.withMutations(mutable => {
|
||||||
|
@ -132,28 +147,28 @@ const expandNormalizedNotifications = (state, notifications, next) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotifications = (state, relationship) => {
|
const filterNotifications = (state: State, relationship: APIEntity) => {
|
||||||
return state.update('items', map => map.filterNot(item => item !== null && item.get('account') === relationship.id));
|
return state.update('items', map => map.filterNot(item => item !== null && item.account === relationship.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterNotificationIds = (state, accountIds, type) => {
|
const filterNotificationIds = (state: State, accountIds: Array<string>, type?: string) => {
|
||||||
const helper = list => list.filterNot(item => item !== null && accountIds.includes(item.get('account')) && (type === undefined || type === item.get('type')));
|
const helper = (list: ImmutableOrderedMap<string, NotificationRecord>) => list.filterNot(item => item !== null && accountIds.includes(item.account as string) && (type === undefined || type === item.type));
|
||||||
return state.update('items', helper);
|
return state.update('items', helper);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateTop = (state, top) => {
|
const updateTop = (state: State, top: boolean) => {
|
||||||
if (top) state = state.set('unread', 0);
|
if (top) state = state.set('unread', 0);
|
||||||
return state.set('top', top);
|
return state.set('top', top);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteByStatus = (state, statusId) => {
|
const deleteByStatus = (state: State, statusId: string) => {
|
||||||
return state.update('items', map => map.filterNot(item => item !== null && item.get('status') === statusId));
|
return state.update('items', map => map.filterNot(item => item !== null && item.status === statusId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateNotificationsQueue = (state, notification, intlMessages, intlLocale) => {
|
const updateNotificationsQueue = (state: State, notification: APIEntity, intlMessages: Record<string, string>, intlLocale: string) => {
|
||||||
const queuedNotifications = state.getIn(['queuedNotifications'], ImmutableOrderedMap());
|
const queuedNotifications = state.queuedNotifications;
|
||||||
const listedNotifications = state.getIn(['items'], ImmutableOrderedMap());
|
const listedNotifications = state.items;
|
||||||
const totalQueuedNotificationsCount = state.getIn(['totalQueuedNotificationsCount'], 0);
|
const totalQueuedNotificationsCount = state.totalQueuedNotificationsCount;
|
||||||
|
|
||||||
const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id);
|
const alreadyExists = queuedNotifications.has(notification.id) || listedNotifications.has(notification.id);
|
||||||
if (alreadyExists) return state;
|
if (alreadyExists) return state;
|
||||||
|
@ -162,25 +177,25 @@ const updateNotificationsQueue = (state, notification, intlMessages, intlLocale)
|
||||||
|
|
||||||
return state.withMutations(mutable => {
|
return state.withMutations(mutable => {
|
||||||
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
if (totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
|
||||||
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, {
|
mutable.set('queuedNotifications', newQueuedNotifications.set(notification.id, QueuedNotificationRecord({
|
||||||
notification,
|
notification,
|
||||||
intlMessages,
|
intlMessages,
|
||||||
intlLocale,
|
intlLocale,
|
||||||
}));
|
})));
|
||||||
}
|
}
|
||||||
mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1);
|
mutable.set('totalQueuedNotificationsCount', totalQueuedNotificationsCount + 1);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const importMarker = (state, marker) => {
|
const importMarker = (state: State, marker: ImmutableMap<string, any>) => {
|
||||||
const lastReadId = marker.getIn(['notifications', 'last_read_id'], -1);
|
const lastReadId = marker.getIn(['notifications', 'last_read_id'], -1) as string | -1;
|
||||||
|
|
||||||
if (!lastReadId) {
|
if (!lastReadId) {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
return state.withMutations(state => {
|
return state.withMutations(state => {
|
||||||
const notifications = state.get('items');
|
const notifications = state.items;
|
||||||
const unread = countFuture(notifications, lastReadId);
|
const unread = countFuture(notifications, lastReadId);
|
||||||
|
|
||||||
state.set('unread', unread);
|
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) {
|
switch (action.type) {
|
||||||
case NOTIFICATIONS_EXPAND_REQUEST:
|
case NOTIFICATIONS_EXPAND_REQUEST:
|
||||||
return state.set('isLoading', true);
|
return state.set('isLoading', true);
|
||||||
case NOTIFICATIONS_EXPAND_FAIL:
|
case NOTIFICATIONS_EXPAND_FAIL:
|
||||||
return state.set('isLoading', false);
|
return state.set('isLoading', false);
|
||||||
case NOTIFICATIONS_FILTER_SET:
|
case NOTIFICATIONS_FILTER_SET:
|
||||||
return state.delete('items').set('hasMore', true);
|
return state.set('items', ImmutableOrderedMap()).set('hasMore', true);
|
||||||
case NOTIFICATIONS_SCROLL_TOP:
|
case NOTIFICATIONS_SCROLL_TOP:
|
||||||
return updateTop(state, action.top);
|
return updateTop(state, action.top);
|
||||||
case NOTIFICATIONS_UPDATE:
|
case NOTIFICATIONS_UPDATE:
|
||||||
|
@ -217,13 +232,13 @@ export default function notifications(state = ReducerRecord(), action) {
|
||||||
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
case FOLLOW_REQUEST_REJECT_SUCCESS:
|
||||||
return filterNotificationIds(state, [action.id], 'follow_request');
|
return filterNotificationIds(state, [action.id], 'follow_request');
|
||||||
case NOTIFICATIONS_CLEAR:
|
case NOTIFICATIONS_CLEAR:
|
||||||
return state.delete('items').set('hasMore', false);
|
return state.set('items', ImmutableOrderedMap()).set('hasMore', false);
|
||||||
case NOTIFICATIONS_MARK_READ_REQUEST:
|
case NOTIFICATIONS_MARK_READ_REQUEST:
|
||||||
return state.set('lastRead', action.lastRead);
|
return state.set('lastRead', action.lastRead);
|
||||||
case MARKER_FETCH_SUCCESS:
|
case MARKER_FETCH_SUCCESS:
|
||||||
case MARKER_SAVE_REQUEST:
|
case MARKER_SAVE_REQUEST:
|
||||||
case MARKER_SAVE_SUCCESS:
|
case MARKER_SAVE_SUCCESS:
|
||||||
return importMarker(state, fromJS(action.marker));
|
return importMarker(state, ImmutableMap(fromJS(action.marker)));
|
||||||
case TIMELINE_DELETE:
|
case TIMELINE_DELETE:
|
||||||
return deleteByStatus(state, action.id);
|
return deleteByStatus(state, action.id);
|
||||||
default:
|
default:
|
Ładowanie…
Reference in New Issue