mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-11 20:21:10 +00:00
Remove react-motion library (#34293)
This commit is contained in:
parent
97b9994743
commit
902aab1245
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { TransitionMotion, spring } from 'react-motion';
|
import { animated, useSpring, config } from '@react-spring/web';
|
||||||
|
|
||||||
import { reduceMotion } from '../initial_state';
|
import { reduceMotion } from '../initial_state';
|
||||||
|
|
||||||
|
@ -11,53 +11,49 @@ interface Props {
|
||||||
}
|
}
|
||||||
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
export const AnimatedNumber: React.FC<Props> = ({ value }) => {
|
||||||
const [previousValue, setPreviousValue] = useState(value);
|
const [previousValue, setPreviousValue] = useState(value);
|
||||||
const [direction, setDirection] = useState<1 | -1>(1);
|
const direction = value > previousValue ? -1 : 1;
|
||||||
|
|
||||||
if (previousValue !== value) {
|
const [styles, api] = useSpring(
|
||||||
setPreviousValue(value);
|
() => ({
|
||||||
setDirection(value > previousValue ? 1 : -1);
|
from: { transform: `translateY(${100 * direction}%)` },
|
||||||
}
|
to: { transform: 'translateY(0%)' },
|
||||||
|
onRest() {
|
||||||
const willEnter = useCallback(() => ({ y: -1 * direction }), [direction]);
|
setPreviousValue(value);
|
||||||
const willLeave = useCallback(
|
},
|
||||||
() => ({ y: spring(1 * direction, { damping: 35, stiffness: 400 }) }),
|
config: { ...config.gentle, duration: 200 },
|
||||||
[direction],
|
immediate: true, // This ensures that the animation is not played when the component is first rendered
|
||||||
|
}),
|
||||||
|
[value, previousValue],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// When the value changes, start the animation
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== previousValue) {
|
||||||
|
void api.start({ reset: true });
|
||||||
|
}
|
||||||
|
}, [api, previousValue, value]);
|
||||||
|
|
||||||
if (reduceMotion) {
|
if (reduceMotion) {
|
||||||
return <ShortNumber value={value} />;
|
return <ShortNumber value={value} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = [
|
|
||||||
{
|
|
||||||
key: `${value}`,
|
|
||||||
data: value,
|
|
||||||
style: { y: spring(0, { damping: 35, stiffness: 400 }) },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TransitionMotion
|
<span className='animated-number'>
|
||||||
styles={styles}
|
<animated.span style={styles}>
|
||||||
willEnter={willEnter}
|
<ShortNumber value={value} />
|
||||||
willLeave={willLeave}
|
</animated.span>
|
||||||
>
|
{value !== previousValue && (
|
||||||
{(items) => (
|
<animated.span
|
||||||
<span className='animated-number'>
|
style={{
|
||||||
{items.map(({ key, data, style }) => (
|
...styles,
|
||||||
<span
|
position: 'absolute',
|
||||||
key={key}
|
top: `${-100 * direction}%`, // Adds extra space on top of translateY
|
||||||
style={{
|
}}
|
||||||
position:
|
role='presentation'
|
||||||
direction * (style.y ?? 0) > 0 ? 'absolute' : 'static',
|
>
|
||||||
transform: `translateY(${(style.y ?? 0) * 100}%)`,
|
<ShortNumber value={previousValue} />
|
||||||
}}
|
</animated.span>
|
||||||
>
|
|
||||||
<ShortNumber value={data as number} />
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</TransitionMotion>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,248 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
|
||||||
|
|
||||||
import escapeTextContentForBrowser from 'escape-html';
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
import emojify from 'mastodon/features/emoji/emoji';
|
|
||||||
import Motion from 'mastodon/features/ui/util/optional_motion';
|
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
|
||||||
|
|
||||||
import { RelativeTimestamp } from './relative_timestamp';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
closed: {
|
|
||||||
id: 'poll.closed',
|
|
||||||
defaultMessage: 'Closed',
|
|
||||||
},
|
|
||||||
voted: {
|
|
||||||
id: 'poll.voted',
|
|
||||||
defaultMessage: 'You voted for this answer',
|
|
||||||
},
|
|
||||||
votes: {
|
|
||||||
id: 'poll.votes',
|
|
||||||
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class Poll extends ImmutablePureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
identity: identityContextPropShape,
|
|
||||||
poll: ImmutablePropTypes.record.isRequired,
|
|
||||||
status: ImmutablePropTypes.map.isRequired,
|
|
||||||
lang: PropTypes.string,
|
|
||||||
intl: PropTypes.object.isRequired,
|
|
||||||
disabled: PropTypes.bool,
|
|
||||||
refresh: PropTypes.func,
|
|
||||||
onVote: PropTypes.func,
|
|
||||||
onInteractionModal: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
selected: {},
|
|
||||||
expired: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
static getDerivedStateFromProps (props, state) {
|
|
||||||
const { poll } = props;
|
|
||||||
const expires_at = poll.get('expires_at');
|
|
||||||
const expired = poll.get('expired') || expires_at !== null && (new Date(expires_at)).getTime() < Date.now();
|
|
||||||
return (expired === state.expired) ? null : { expired };
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this._setupTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate () {
|
|
||||||
this._setupTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
clearTimeout(this._timer);
|
|
||||||
}
|
|
||||||
|
|
||||||
_setupTimer () {
|
|
||||||
const { poll } = this.props;
|
|
||||||
clearTimeout(this._timer);
|
|
||||||
if (!this.state.expired) {
|
|
||||||
const delay = (new Date(poll.get('expires_at'))).getTime() - Date.now();
|
|
||||||
this._timer = setTimeout(() => {
|
|
||||||
this.setState({ expired: true });
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_toggleOption = value => {
|
|
||||||
if (this.props.poll.get('multiple')) {
|
|
||||||
const tmp = { ...this.state.selected };
|
|
||||||
if (tmp[value]) {
|
|
||||||
delete tmp[value];
|
|
||||||
} else {
|
|
||||||
tmp[value] = true;
|
|
||||||
}
|
|
||||||
this.setState({ selected: tmp });
|
|
||||||
} else {
|
|
||||||
const tmp = {};
|
|
||||||
tmp[value] = true;
|
|
||||||
this.setState({ selected: tmp });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOptionChange = ({ target: { value } }) => {
|
|
||||||
this._toggleOption(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOptionKeyPress = (e) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
this._toggleOption(e.target.getAttribute('data-index'));
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleVote = () => {
|
|
||||||
if (this.props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.identity.signedIn) {
|
|
||||||
this.props.onVote(Object.keys(this.state.selected));
|
|
||||||
} else {
|
|
||||||
this.props.onInteractionModal('vote', this.props.status);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handleRefresh = () => {
|
|
||||||
if (this.props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
handleReveal = () => {
|
|
||||||
this.setState({ revealed: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
renderOption (option, optionIndex, showResults) {
|
|
||||||
const { poll, lang, disabled, intl } = this.props;
|
|
||||||
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
|
||||||
const percent = pollVotesCount === 0 ? 0 : (option.get('votes_count') / pollVotesCount) * 100;
|
|
||||||
const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') >= other.get('votes_count'));
|
|
||||||
const active = !!this.state.selected[`${optionIndex}`];
|
|
||||||
const voted = option.get('voted') || (poll.get('own_votes') && poll.get('own_votes').includes(optionIndex));
|
|
||||||
|
|
||||||
const title = option.getIn(['translation', 'title']) || option.get('title');
|
|
||||||
let titleHtml = option.getIn(['translation', 'titleHtml']) || option.get('titleHtml');
|
|
||||||
|
|
||||||
if (!titleHtml) {
|
|
||||||
const emojiMap = emojiMap(poll);
|
|
||||||
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={option.get('title')}>
|
|
||||||
<label className={classNames('poll__option', { selectable: !showResults })}>
|
|
||||||
<input
|
|
||||||
name='vote-options'
|
|
||||||
type={poll.get('multiple') ? 'checkbox' : 'radio'}
|
|
||||||
value={optionIndex}
|
|
||||||
checked={active}
|
|
||||||
onChange={this.handleOptionChange}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!showResults && (
|
|
||||||
<span
|
|
||||||
className={classNames('poll__input', { checkbox: poll.get('multiple'), active })}
|
|
||||||
tabIndex={0}
|
|
||||||
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
|
||||||
onKeyPress={this.handleOptionKeyPress}
|
|
||||||
aria-checked={active}
|
|
||||||
aria-label={title}
|
|
||||||
lang={lang}
|
|
||||||
data-index={optionIndex}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showResults && (
|
|
||||||
<span
|
|
||||||
className='poll__number'
|
|
||||||
title={intl.formatMessage(messages.votes, {
|
|
||||||
votes: option.get('votes_count'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{Math.round(percent)}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<span
|
|
||||||
className='poll__option__text translate'
|
|
||||||
lang={lang}
|
|
||||||
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!!voted && <span className='poll__voted'>
|
|
||||||
<Icon id='check' icon={CheckIcon} className='poll__voted__mark' title={intl.formatMessage(messages.voted)} />
|
|
||||||
</span>}
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{showResults && (
|
|
||||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { stiffness: 180, damping: 12 }) }}>
|
|
||||||
{({ width }) =>
|
|
||||||
<span className={classNames('poll__chart', { leading })} style={{ width: `${width}%` }} />
|
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { poll, intl } = this.props;
|
|
||||||
const { revealed, expired } = this.state;
|
|
||||||
|
|
||||||
if (!poll) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeRemaining = expired ? intl.formatMessage(messages.closed) : <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
|
||||||
const showResults = poll.get('voted') || revealed || expired;
|
|
||||||
const disabled = this.props.disabled || Object.entries(this.state.selected).every(item => !item);
|
|
||||||
|
|
||||||
let votesCount = null;
|
|
||||||
|
|
||||||
if (poll.get('voters_count') !== null && poll.get('voters_count') !== undefined) {
|
|
||||||
votesCount = <FormattedMessage id='poll.total_people' defaultMessage='{count, plural, one {# person} other {# people}}' values={{ count: poll.get('voters_count') }} />;
|
|
||||||
} else {
|
|
||||||
votesCount = <FormattedMessage id='poll.total_votes' defaultMessage='{count, plural, one {# vote} other {# votes}}' values={{ count: poll.get('votes_count') }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='poll'>
|
|
||||||
<ul>
|
|
||||||
{poll.get('options').map((option, i) => this.renderOption(option, i, showResults))}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<div className='poll__footer'>
|
|
||||||
{!showResults && <button className='button button-secondary' disabled={disabled} onClick={this.handleVote}><FormattedMessage id='poll.vote' defaultMessage='Vote' /></button>}
|
|
||||||
{!showResults && <><button className='poll__link' onClick={this.handleReveal}><FormattedMessage id='poll.reveal' defaultMessage='See results' /></button> · </>}
|
|
||||||
{showResults && !this.props.disabled && <><button className='poll__link' onClick={this.handleRefresh}><FormattedMessage id='poll.refresh' defaultMessage='Refresh' /></button> · </>}
|
|
||||||
{votesCount}
|
|
||||||
{poll.get('expires_at') && <> · {timeRemaining}</>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default injectIntl(withIdentity(Poll));
|
|
352
app/javascript/mastodon/components/poll.tsx
Normal file
352
app/javascript/mastodon/components/poll.tsx
Normal file
|
@ -0,0 +1,352 @@
|
||||||
|
import type { KeyboardEventHandler } from 'react';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import escapeTextContentForBrowser from 'escape-html';
|
||||||
|
import { debounce } from 'lodash';
|
||||||
|
|
||||||
|
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
|
||||||
|
import { openModal } from 'mastodon/actions/modal';
|
||||||
|
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import emojify from 'mastodon/features/emoji/emoji';
|
||||||
|
import { useIdentity } from 'mastodon/identity_context';
|
||||||
|
import { reduceMotion } from 'mastodon/initial_state';
|
||||||
|
import { makeEmojiMap } from 'mastodon/models/custom_emoji';
|
||||||
|
import type * as Model from 'mastodon/models/poll';
|
||||||
|
import type { Status } from 'mastodon/models/status';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||||
|
|
||||||
|
import { RelativeTimestamp } from './relative_timestamp';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
closed: {
|
||||||
|
id: 'poll.closed',
|
||||||
|
defaultMessage: 'Closed',
|
||||||
|
},
|
||||||
|
voted: {
|
||||||
|
id: 'poll.voted',
|
||||||
|
defaultMessage: 'You voted for this answer',
|
||||||
|
},
|
||||||
|
votes: {
|
||||||
|
id: 'poll.votes',
|
||||||
|
defaultMessage: '{votes, plural, one {# vote} other {# votes}}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface PollProps {
|
||||||
|
pollId: string;
|
||||||
|
status: Status;
|
||||||
|
lang?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Poll: React.FC<PollProps> = (props) => {
|
||||||
|
const { pollId, status } = props;
|
||||||
|
|
||||||
|
// Third party hooks
|
||||||
|
const poll = useAppSelector((state) => state.polls.get(pollId));
|
||||||
|
const identity = useIdentity();
|
||||||
|
const intl = useIntl();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const [revealed, setRevealed] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
// Derived values
|
||||||
|
const expired = useMemo(() => {
|
||||||
|
if (!poll) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expiresAt = poll.get('expires_at');
|
||||||
|
return poll.get('expired') || new Date(expiresAt).getTime() < Date.now();
|
||||||
|
}, [poll]);
|
||||||
|
const timeRemaining = useMemo(() => {
|
||||||
|
if (!poll) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (expired) {
|
||||||
|
return intl.formatMessage(messages.closed);
|
||||||
|
}
|
||||||
|
return <RelativeTimestamp timestamp={poll.get('expires_at')} futureDate />;
|
||||||
|
}, [expired, intl, poll]);
|
||||||
|
const votesCount = useMemo(() => {
|
||||||
|
if (!poll) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (poll.get('voters_count')) {
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='poll.total_people'
|
||||||
|
defaultMessage='{count, plural, one {# person} other {# people}}'
|
||||||
|
values={{ count: poll.get('voters_count') }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='poll.total_votes'
|
||||||
|
defaultMessage='{count, plural, one {# vote} other {# votes}}'
|
||||||
|
values={{ count: poll.get('votes_count') }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, [poll]);
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
props.disabled || Object.values(selected).every((item) => !item);
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
const handleVote = useCallback(() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identity.signedIn) {
|
||||||
|
void dispatch(vote({ pollId, choices: Object.keys(selected) }));
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
openModal({
|
||||||
|
modalType: 'INTERACTION',
|
||||||
|
modalProps: {
|
||||||
|
type: 'vote',
|
||||||
|
accountId: status.getIn(['account', 'id']),
|
||||||
|
url: status.get('uri'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [disabled, dispatch, identity, pollId, selected, status]);
|
||||||
|
|
||||||
|
const handleReveal = useCallback(() => {
|
||||||
|
setRevealed(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRefresh = useCallback(() => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debounce(
|
||||||
|
() => {
|
||||||
|
void dispatch(fetchPoll({ pollId }));
|
||||||
|
},
|
||||||
|
1000,
|
||||||
|
{ leading: true },
|
||||||
|
);
|
||||||
|
}, [disabled, dispatch, pollId]);
|
||||||
|
|
||||||
|
const handleOptionChange = useCallback(
|
||||||
|
(choiceIndex: number) => {
|
||||||
|
if (!poll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (poll.get('multiple')) {
|
||||||
|
setSelected((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[choiceIndex]: !prev[choiceIndex],
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
setSelected({ [choiceIndex]: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[poll],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!poll) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const showResults = poll.get('voted') || revealed || expired;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='poll'>
|
||||||
|
<ul>
|
||||||
|
{poll.get('options').map((option, i) => (
|
||||||
|
<PollOption
|
||||||
|
key={option.get('title') || i}
|
||||||
|
index={i}
|
||||||
|
poll={poll}
|
||||||
|
option={option}
|
||||||
|
showResults={showResults}
|
||||||
|
active={!!selected[i]}
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className='poll__footer'>
|
||||||
|
{!showResults && (
|
||||||
|
<button
|
||||||
|
className='button button-secondary'
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleVote}
|
||||||
|
>
|
||||||
|
<FormattedMessage id='poll.vote' defaultMessage='Vote' />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!showResults && (
|
||||||
|
<>
|
||||||
|
<button className='poll__link' onClick={handleReveal}>
|
||||||
|
<FormattedMessage id='poll.reveal' defaultMessage='See results' />
|
||||||
|
</button>{' '}
|
||||||
|
·{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showResults && !disabled && (
|
||||||
|
<>
|
||||||
|
<button className='poll__link' onClick={handleRefresh}>
|
||||||
|
<FormattedMessage id='poll.refresh' defaultMessage='Refresh' />
|
||||||
|
</button>{' '}
|
||||||
|
·{' '}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{votesCount}
|
||||||
|
{poll.get('expires_at') && <> · {timeRemaining}</>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type PollOptionProps = Pick<PollProps, 'disabled' | 'lang'> & {
|
||||||
|
active: boolean;
|
||||||
|
onChange: (index: number) => void;
|
||||||
|
poll: Model.Poll;
|
||||||
|
option: Model.PollOption;
|
||||||
|
index: number;
|
||||||
|
showResults?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PollOption: React.FC<PollOptionProps> = (props) => {
|
||||||
|
const { active, lang, disabled, poll, option, index, showResults, onChange } =
|
||||||
|
props;
|
||||||
|
const voted = option.get('voted') || poll.get('own_votes')?.includes(index);
|
||||||
|
const title =
|
||||||
|
(option.getIn(['translation', 'title']) as string) || option.get('title');
|
||||||
|
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
// Derived values
|
||||||
|
const percent = useMemo(() => {
|
||||||
|
const pollVotesCount = poll.get('voters_count') || poll.get('votes_count');
|
||||||
|
return pollVotesCount === 0
|
||||||
|
? 0
|
||||||
|
: (option.get('votes_count') / pollVotesCount) * 100;
|
||||||
|
}, [option, poll]);
|
||||||
|
const isLeading = useMemo(
|
||||||
|
() =>
|
||||||
|
poll
|
||||||
|
.get('options')
|
||||||
|
.filterNot((other) => other.get('title') === option.get('title'))
|
||||||
|
.every(
|
||||||
|
(other) => option.get('votes_count') >= other.get('votes_count'),
|
||||||
|
),
|
||||||
|
[poll, option],
|
||||||
|
);
|
||||||
|
const titleHtml = useMemo(() => {
|
||||||
|
let titleHtml =
|
||||||
|
(option.getIn(['translation', 'titleHtml']) as string) ||
|
||||||
|
option.get('titleHtml');
|
||||||
|
|
||||||
|
if (!titleHtml) {
|
||||||
|
const emojiMap = makeEmojiMap(poll.get('emojis'));
|
||||||
|
titleHtml = emojify(escapeTextContentForBrowser(title), emojiMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return titleHtml;
|
||||||
|
}, [option, poll, title]);
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleOptionChange = useCallback(() => {
|
||||||
|
onChange(index);
|
||||||
|
}, [index, onChange]);
|
||||||
|
const handleOptionKeyPress: KeyboardEventHandler = useCallback(
|
||||||
|
(event) => {
|
||||||
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
|
onChange(index);
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[index, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const widthSpring = useSpring({
|
||||||
|
from: {
|
||||||
|
width: '0%',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
width: `${percent}%`,
|
||||||
|
},
|
||||||
|
immediate: reduceMotion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<label
|
||||||
|
className={classNames('poll__option', { selectable: !showResults })}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name='vote-options'
|
||||||
|
type={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||||
|
value={index}
|
||||||
|
checked={active}
|
||||||
|
onChange={handleOptionChange}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!showResults && (
|
||||||
|
<span
|
||||||
|
className={classNames('poll__input', {
|
||||||
|
checkbox: poll.get('multiple'),
|
||||||
|
active,
|
||||||
|
})}
|
||||||
|
tabIndex={0}
|
||||||
|
role={poll.get('multiple') ? 'checkbox' : 'radio'}
|
||||||
|
onKeyDown={handleOptionKeyPress}
|
||||||
|
aria-checked={active}
|
||||||
|
aria-label={title}
|
||||||
|
lang={lang}
|
||||||
|
data-index={index}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{showResults && (
|
||||||
|
<span
|
||||||
|
className='poll__number'
|
||||||
|
title={intl.formatMessage(messages.votes, {
|
||||||
|
votes: option.get('votes_count'),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Math.round(percent)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
className='poll__option__text translate'
|
||||||
|
lang={lang}
|
||||||
|
dangerouslySetInnerHTML={{ __html: titleHtml }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!!voted && (
|
||||||
|
<span className='poll__voted'>
|
||||||
|
<Icon
|
||||||
|
id='check'
|
||||||
|
icon={CheckIcon}
|
||||||
|
className='poll__voted__mark'
|
||||||
|
title={intl.formatMessage(messages.voted)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{showResults && (
|
||||||
|
<animated.span
|
||||||
|
className={classNames('poll__chart', { leading: isLeading })}
|
||||||
|
style={widthSpring}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
|
@ -11,7 +11,7 @@ import { connect } from 'react-redux';
|
||||||
|
|
||||||
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import PollContainer from 'mastodon/containers/poll_container';
|
import { Poll } from 'mastodon/components/poll';
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||||
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
|
import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
@ -245,7 +245,7 @@ class StatusContent extends PureComponent {
|
||||||
);
|
);
|
||||||
|
|
||||||
const poll = !!status.get('poll') && (
|
const poll = !!status.get('poll') && (
|
||||||
<PollContainer pollId={status.get('poll')} status={status} lang={language} />
|
<Poll pollId={status.get('poll')} status={status} lang={language} />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.props.onClick) {
|
if (this.props.onClick) {
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { fromJS } from 'immutable';
|
||||||
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
|
||||||
import MediaGallery from 'mastodon/components/media_gallery';
|
import MediaGallery from 'mastodon/components/media_gallery';
|
||||||
import ModalRoot from 'mastodon/components/modal_root';
|
import ModalRoot from 'mastodon/components/modal_root';
|
||||||
import Poll from 'mastodon/components/poll';
|
import { Poll } from 'mastodon/components/poll';
|
||||||
import Audio from 'mastodon/features/audio';
|
import Audio from 'mastodon/features/audio';
|
||||||
import Card from 'mastodon/features/status/components/card';
|
import Card from 'mastodon/features/status/components/card';
|
||||||
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
import MediaModal from 'mastodon/features/ui/components/media_modal';
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
|
|
||||||
import { openModal } from 'mastodon/actions/modal';
|
|
||||||
import { fetchPoll, vote } from 'mastodon/actions/polls';
|
|
||||||
import Poll from 'mastodon/components/poll';
|
|
||||||
|
|
||||||
const mapDispatchToProps = (dispatch, { pollId }) => ({
|
|
||||||
refresh: debounce(
|
|
||||||
() => {
|
|
||||||
dispatch(fetchPoll({ pollId }));
|
|
||||||
},
|
|
||||||
1000,
|
|
||||||
{ leading: true },
|
|
||||||
),
|
|
||||||
|
|
||||||
onVote (choices) {
|
|
||||||
dispatch(vote({ pollId, choices }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onInteractionModal (type, status) {
|
|
||||||
dispatch(openModal({
|
|
||||||
modalType: 'INTERACTION',
|
|
||||||
modalProps: {
|
|
||||||
type,
|
|
||||||
accountId: status.getIn(['account', 'id']),
|
|
||||||
url: status.get('uri'),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapStateToProps = (state, { pollId }) => ({
|
|
||||||
poll: state.polls.get(pollId),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(Poll);
|
|
|
@ -20,7 +20,6 @@ import PollButtonContainer from '../containers/poll_button_container';
|
||||||
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
|
||||||
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
import SpoilerButtonContainer from '../containers/spoiler_button_container';
|
||||||
import UploadButtonContainer from '../containers/upload_button_container';
|
import UploadButtonContainer from '../containers/upload_button_container';
|
||||||
import WarningContainer from '../containers/warning_container';
|
|
||||||
import { countableText } from '../util/counter';
|
import { countableText } from '../util/counter';
|
||||||
|
|
||||||
import { CharacterCounter } from './character_counter';
|
import { CharacterCounter } from './character_counter';
|
||||||
|
@ -30,6 +29,7 @@ import { NavigationBar } from './navigation_bar';
|
||||||
import { PollForm } from "./poll_form";
|
import { PollForm } from "./poll_form";
|
||||||
import { ReplyIndicator } from './reply_indicator';
|
import { ReplyIndicator } from './reply_indicator';
|
||||||
import { UploadForm } from './upload_form';
|
import { UploadForm } from './upload_form';
|
||||||
|
import { Warning } from './warning';
|
||||||
|
|
||||||
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
|
||||||
|
|
||||||
|
@ -233,7 +233,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||||
<form className='compose-form' onSubmit={this.handleSubmit}>
|
<form className='compose-form' onSubmit={this.handleSubmit}>
|
||||||
<ReplyIndicator />
|
<ReplyIndicator />
|
||||||
{!withoutNavigation && <NavigationBar />}
|
{!withoutNavigation && <NavigationBar />}
|
||||||
<WarningContainer />
|
<Warning />
|
||||||
|
|
||||||
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
|
<div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}>
|
||||||
<div className='compose-form__scrollable'>
|
<div className='compose-form__scrollable'>
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
|
|
||||||
import { Icon } from 'mastodon/components/icon';
|
|
||||||
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
|
||||||
|
|
||||||
export const UploadProgress = ({ active, progress, isProcessing }) => {
|
|
||||||
if (!active) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let message;
|
|
||||||
|
|
||||||
if (isProcessing) {
|
|
||||||
message = <FormattedMessage id='upload_progress.processing' defaultMessage='Processing…' />;
|
|
||||||
} else {
|
|
||||||
message = <FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='upload-progress'>
|
|
||||||
<Icon id='upload' icon={UploadFileIcon} />
|
|
||||||
|
|
||||||
<div className='upload-progress__message'>
|
|
||||||
{message}
|
|
||||||
|
|
||||||
<div className='upload-progress__backdrop'>
|
|
||||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
|
||||||
{({ width }) =>
|
|
||||||
<div className='upload-progress__tracker' style={{ width: `${width}%` }} />
|
|
||||||
}
|
|
||||||
</Motion>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
UploadProgress.propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
progress: PropTypes.number,
|
|
||||||
isProcessing: PropTypes.bool,
|
|
||||||
};
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
|
import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react';
|
||||||
|
import { Icon } from 'mastodon/components/icon';
|
||||||
|
import { reduceMotion } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
interface UploadProgressProps {
|
||||||
|
active: boolean;
|
||||||
|
progress: number;
|
||||||
|
isProcessing: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadProgress: React.FC<UploadProgressProps> = ({
|
||||||
|
active,
|
||||||
|
progress,
|
||||||
|
isProcessing,
|
||||||
|
}) => {
|
||||||
|
const styles = useSpring({
|
||||||
|
from: { width: '0%' },
|
||||||
|
to: { width: `${progress}%` },
|
||||||
|
reset: true,
|
||||||
|
immediate: reduceMotion,
|
||||||
|
});
|
||||||
|
if (!active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message;
|
||||||
|
|
||||||
|
if (isProcessing) {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='upload_progress.processing'
|
||||||
|
defaultMessage='Processing…'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
message = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='upload_progress.label'
|
||||||
|
defaultMessage='Uploading…'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='upload-progress'>
|
||||||
|
<Icon id='upload' icon={UploadFileIcon} />
|
||||||
|
|
||||||
|
<div className='upload-progress__message'>
|
||||||
|
{message}
|
||||||
|
|
||||||
|
<div className='upload-progress__backdrop'>
|
||||||
|
<animated.div className='upload-progress__tracker' style={styles} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,28 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import Motion from '../../ui/util/optional_motion';
|
|
||||||
|
|
||||||
export default class Warning extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
message: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { message } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}>
|
|
||||||
{({ opacity, scaleX, scaleY }) => (
|
|
||||||
<div className='compose-form__warning' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}>
|
|
||||||
{message}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
|
import { me } from 'mastodon/initial_state';
|
||||||
|
import { useAppSelector } from 'mastodon/store';
|
||||||
|
import type { RootState } from 'mastodon/store';
|
||||||
|
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
|
||||||
|
|
||||||
|
const selector = createSelector(
|
||||||
|
(state: RootState) => state.compose.get('privacy') as string,
|
||||||
|
(state: RootState) => !!state.compose.getIn(['accounts', me, 'locked']),
|
||||||
|
(state: RootState) => state.compose.get('text') as string,
|
||||||
|
(privacy, locked, text) => ({
|
||||||
|
needsLockWarning: privacy === 'private' && !locked,
|
||||||
|
hashtagWarning: privacy !== 'public' && HASHTAG_PATTERN_REGEX.test(text),
|
||||||
|
directMessageWarning: privacy === 'direct',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const Warning = () => {
|
||||||
|
const { needsLockWarning, hashtagWarning, directMessageWarning } =
|
||||||
|
useAppSelector(selector);
|
||||||
|
if (needsLockWarning) {
|
||||||
|
return (
|
||||||
|
<WarningMessage>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.lock_disclaimer'
|
||||||
|
defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.'
|
||||||
|
values={{
|
||||||
|
locked: (
|
||||||
|
<a href='/settings/profile'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.lock_disclaimer.lock'
|
||||||
|
defaultMessage='locked'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WarningMessage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashtagWarning) {
|
||||||
|
return (
|
||||||
|
<WarningMessage>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.hashtag_warning'
|
||||||
|
defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag."
|
||||||
|
/>
|
||||||
|
</WarningMessage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directMessageWarning) {
|
||||||
|
return (
|
||||||
|
<WarningMessage>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.encryption_warning'
|
||||||
|
defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.'
|
||||||
|
/>{' '}
|
||||||
|
<a href='/terms' target='_blank'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='compose_form.direct_message_warning_learn_more'
|
||||||
|
defaultMessage='Learn more'
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</WarningMessage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WarningMessage: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const styles = useSpring({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
transform: 'scale(0.85, 0.75)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
transform: 'scale(1, 1)',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<animated.div className='compose-form__warning' style={styles}>
|
||||||
|
{children}
|
||||||
|
</animated.div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,46 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { me } from 'mastodon/initial_state';
|
|
||||||
import { HASHTAG_PATTERN_REGEX } from 'mastodon/utils/hashtags';
|
|
||||||
|
|
||||||
import Warning from '../components/warning';
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']),
|
|
||||||
hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && HASHTAG_PATTERN_REGEX.test(state.getIn(['compose', 'text'])),
|
|
||||||
directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct',
|
|
||||||
});
|
|
||||||
|
|
||||||
const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => {
|
|
||||||
if (needsLockWarning) {
|
|
||||||
return <Warning message={<FormattedMessage id='compose_form.lock_disclaimer' defaultMessage='Your account is not {locked}. Anyone can follow you to view your follower-only posts.' values={{ locked: <a href='/settings/profile'><FormattedMessage id='compose_form.lock_disclaimer.lock' defaultMessage='locked' /></a> }} />} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hashtagWarning) {
|
|
||||||
return <Warning message={<FormattedMessage id='compose_form.hashtag_warning' defaultMessage="This post won't be listed under any hashtag as it is unlisted. Only public posts can be searched by hashtag." />} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (directMessageWarning) {
|
|
||||||
const message = (
|
|
||||||
<span>
|
|
||||||
<FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
|
|
||||||
return <Warning message={message} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
WarningWrapper.propTypes = {
|
|
||||||
needsLockWarning: PropTypes.bool,
|
|
||||||
hashtagWarning: PropTypes.bool,
|
|
||||||
directMessageWarning: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(mapStateToProps)(WarningWrapper);
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl';
|
||||||
|
|
||||||
|
@ -9,8 +9,7 @@ import { withRouter } from 'react-router-dom';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
|
||||||
import TransitionMotion from 'react-motion/lib/TransitionMotion';
|
import { animated, useTransition } from '@react-spring/web';
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
import ReactSwipeableViews from 'react-swipeable-views';
|
import ReactSwipeableViews from 'react-swipeable-views';
|
||||||
|
|
||||||
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
import elephantUIPlane from '@/images/elephant_ui_plane.svg';
|
||||||
|
@ -239,72 +238,76 @@ class Reaction extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
|
<animated.button className={classNames('reactions-bar__item', { active: reaction.get('me') })} onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} title={`:${shortCode}:`} style={this.props.style}>
|
||||||
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
|
<span className='reactions-bar__item__emoji'><Emoji hovered={this.state.hovered} emoji={reaction.get('name')} emojiMap={this.props.emojiMap} /></span>
|
||||||
<span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
|
<span className='reactions-bar__item__count'><AnimatedNumber value={reaction.get('count')} /></span>
|
||||||
</button>
|
</animated.button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReactionsBar extends ImmutablePureComponent {
|
const ReactionsBar = ({
|
||||||
|
announcementId,
|
||||||
|
reactions,
|
||||||
|
emojiMap,
|
||||||
|
addReaction,
|
||||||
|
removeReaction,
|
||||||
|
}) => {
|
||||||
|
const visibleReactions = useMemo(() => reactions.filter(x => x.get('count') > 0).toArray(), [reactions]);
|
||||||
|
|
||||||
static propTypes = {
|
const handleEmojiPick = useCallback((emoji) => {
|
||||||
announcementId: PropTypes.string.isRequired,
|
addReaction(announcementId, emoji.native.replaceAll(/:/g, ''));
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
}, [addReaction, announcementId]);
|
||||||
addReaction: PropTypes.func.isRequired,
|
|
||||||
removeReaction: PropTypes.func.isRequired,
|
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleEmojiPick = data => {
|
const transitions = useTransition(visibleReactions, {
|
||||||
const { addReaction, announcementId } = this.props;
|
from: {
|
||||||
addReaction(announcementId, data.native.replace(/:/g, ''));
|
scale: 0,
|
||||||
};
|
},
|
||||||
|
enter: {
|
||||||
|
scale: 1,
|
||||||
|
},
|
||||||
|
leave: {
|
||||||
|
scale: 0,
|
||||||
|
},
|
||||||
|
immediate: reduceMotion,
|
||||||
|
keys: visibleReactions.map(x => x.get('name')),
|
||||||
|
});
|
||||||
|
|
||||||
willEnter () {
|
return (
|
||||||
return { scale: reduceMotion ? 1 : 0 };
|
<div
|
||||||
}
|
className={classNames('reactions-bar', {
|
||||||
|
'reactions-bar--empty': visibleReactions.length === 0
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{transitions(({ scale }, reaction) => (
|
||||||
|
<Reaction
|
||||||
|
key={reaction.get('name')}
|
||||||
|
reaction={reaction}
|
||||||
|
style={{ transform: scale.to((s) => `scale(${s})`) }}
|
||||||
|
addReaction={addReaction}
|
||||||
|
removeReaction={removeReaction}
|
||||||
|
announcementId={announcementId}
|
||||||
|
emojiMap={emojiMap}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
willLeave () {
|
{visibleReactions.length < 8 && (
|
||||||
return { scale: reduceMotion ? 0 : spring(0, { stiffness: 170, damping: 26 }) };
|
<EmojiPickerDropdown
|
||||||
}
|
onPickEmoji={handleEmojiPick}
|
||||||
|
button={<Icon id='plus' icon={AddIcon} />}
|
||||||
render () {
|
/>
|
||||||
const { reactions } = this.props;
|
)}
|
||||||
const visibleReactions = reactions.filter(x => x.get('count') > 0);
|
</div>
|
||||||
|
);
|
||||||
const styles = visibleReactions.map(reaction => ({
|
};
|
||||||
key: reaction.get('name'),
|
ReactionsBar.propTypes = {
|
||||||
data: reaction,
|
announcementId: PropTypes.string.isRequired,
|
||||||
style: { scale: reduceMotion ? 1 : spring(1, { stiffness: 150, damping: 13 }) },
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
})).toArray();
|
addReaction: PropTypes.func.isRequired,
|
||||||
|
removeReaction: PropTypes.func.isRequired,
|
||||||
return (
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
<TransitionMotion styles={styles} willEnter={this.willEnter} willLeave={this.willLeave}>
|
};
|
||||||
{items => (
|
|
||||||
<div className={classNames('reactions-bar', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
|
||||||
{items.map(({ key, data, style }) => (
|
|
||||||
<Reaction
|
|
||||||
key={key}
|
|
||||||
reaction={data}
|
|
||||||
style={{ transform: `scale(${style.scale})`, position: style.scale < 0.5 ? 'absolute' : 'static' }}
|
|
||||||
announcementId={this.props.announcementId}
|
|
||||||
addReaction={this.props.addReaction}
|
|
||||||
removeReaction={this.props.removeReaction}
|
|
||||||
emojiMap={this.props.emojiMap}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{visibleReactions.size < 8 && <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} button={<Icon id='plus' icon={AddIcon} />} />}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</TransitionMotion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Announcement extends ImmutablePureComponent {
|
class Announcement extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { PureComponent } from 'react';
|
|
||||||
|
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import spring from 'react-motion/lib/spring';
|
|
||||||
|
|
||||||
import Motion from '../util/optional_motion';
|
|
||||||
|
|
||||||
export default class UploadArea extends PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
active: PropTypes.bool,
|
|
||||||
onClose: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleKeyUp = (e) => {
|
|
||||||
const keyCode = e.keyCode;
|
|
||||||
if (this.props.active) {
|
|
||||||
switch(keyCode) {
|
|
||||||
case 27:
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
this.props.onClose();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
window.addEventListener('keyup', this.handleKeyUp, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
window.removeEventListener('keyup', this.handleKeyUp);
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { active } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Motion defaultStyle={{ backgroundOpacity: 0, backgroundScale: 0.95 }} style={{ backgroundOpacity: spring(active ? 1 : 0, { stiffness: 150, damping: 15 }), backgroundScale: spring(active ? 1 : 0.95, { stiffness: 200, damping: 3 }) }}>
|
|
||||||
{({ backgroundOpacity, backgroundScale }) => (
|
|
||||||
<div className='upload-area' style={{ visibility: active ? 'visible' : 'hidden', opacity: backgroundOpacity }}>
|
|
||||||
<div className='upload-area__drop'>
|
|
||||||
<div className='upload-area__background' style={{ transform: `scale(${backgroundScale})` }} />
|
|
||||||
<div className='upload-area__content'><FormattedMessage id='upload_area.title' defaultMessage='Drag & drop to upload' /></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
|
import { animated, config, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
|
import { reduceMotion } from 'mastodon/initial_state';
|
||||||
|
|
||||||
|
interface UploadAreaProps {
|
||||||
|
active?: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UploadArea: React.FC<UploadAreaProps> = ({ active, onClose }) => {
|
||||||
|
const handleKeyUp = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (active && e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[active, onClose],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('keyup', handleKeyUp, false);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
};
|
||||||
|
}, [handleKeyUp]);
|
||||||
|
|
||||||
|
const wrapperAnimStyles = useSpring({
|
||||||
|
from: {
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
opacity: 1,
|
||||||
|
},
|
||||||
|
reverse: !active,
|
||||||
|
immediate: reduceMotion,
|
||||||
|
});
|
||||||
|
const backgroundAnimStyles = useSpring({
|
||||||
|
from: {
|
||||||
|
transform: 'scale(0.95)',
|
||||||
|
},
|
||||||
|
to: {
|
||||||
|
transform: 'scale(1)',
|
||||||
|
},
|
||||||
|
reverse: !active,
|
||||||
|
config: config.wobbly,
|
||||||
|
immediate: reduceMotion,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<animated.div
|
||||||
|
className='upload-area'
|
||||||
|
style={{
|
||||||
|
...wrapperAnimStyles,
|
||||||
|
visibility: active ? 'visible' : 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className='upload-area__drop'>
|
||||||
|
<animated.div
|
||||||
|
className='upload-area__background'
|
||||||
|
style={backgroundAnimStyles}
|
||||||
|
/>
|
||||||
|
<div className='upload-area__content'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='upload_area.title'
|
||||||
|
defaultMessage='Drag & drop to upload'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</animated.div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -30,7 +30,7 @@ import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding
|
||||||
|
|
||||||
import BundleColumnError from './components/bundle_column_error';
|
import BundleColumnError from './components/bundle_column_error';
|
||||||
import Header from './components/header';
|
import Header from './components/header';
|
||||||
import UploadArea from './components/upload_area';
|
import { UploadArea } from './components/upload_area';
|
||||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||||
import LoadingBarContainer from './containers/loading_bar_container';
|
import LoadingBarContainer from './containers/loading_bar_container';
|
||||||
import ModalContainer from './containers/modal_container';
|
import ModalContainer from './containers/modal_container';
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
import Motion from 'react-motion/lib/Motion';
|
|
||||||
|
|
||||||
import { reduceMotion } from '../../../initial_state';
|
|
||||||
|
|
||||||
import ReducedMotion from './reduced_motion';
|
|
||||||
|
|
||||||
export default reduceMotion ? ReducedMotion : Motion;
|
|
|
@ -1,45 +0,0 @@
|
||||||
// Like react-motion's Motion, but reduces all animations to cross-fades
|
|
||||||
// for the benefit of users with motion sickness.
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Component } from 'react';
|
|
||||||
|
|
||||||
import Motion from 'react-motion/lib/Motion';
|
|
||||||
|
|
||||||
const stylesToKeep = ['opacity', 'backgroundOpacity'];
|
|
||||||
|
|
||||||
const extractValue = (value) => {
|
|
||||||
// This is either an object with a "val" property or it's a number
|
|
||||||
return (typeof value === 'object' && value && 'val' in value) ? value.val : value;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ReducedMotion extends Component {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
defaultStyle: PropTypes.object,
|
|
||||||
style: PropTypes.object,
|
|
||||||
children: PropTypes.func,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
|
|
||||||
const { style, defaultStyle, children } = this.props;
|
|
||||||
|
|
||||||
Object.keys(style).forEach(key => {
|
|
||||||
if (stylesToKeep.includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If it's setting an x or height or scale or some other value, we need
|
|
||||||
// to preserve the end-state value without actually animating it
|
|
||||||
style[key] = defaultStyle[key] = extractValue(style[key]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Motion style={style} defaultStyle={defaultStyle}>
|
|
||||||
{children}
|
|
||||||
</Motion>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ReducedMotion;
|
|
|
@ -104,7 +104,6 @@
|
||||||
"react-immutable-proptypes": "^2.2.0",
|
"react-immutable-proptypes": "^2.2.0",
|
||||||
"react-immutable-pure-component": "^2.2.2",
|
"react-immutable-pure-component": "^2.2.2",
|
||||||
"react-intl": "^7.0.0",
|
"react-intl": "^7.0.0",
|
||||||
"react-motion": "^0.5.2",
|
|
||||||
"react-overlays": "^5.2.1",
|
"react-overlays": "^5.2.1",
|
||||||
"react-redux": "^9.0.4",
|
"react-redux": "^9.0.4",
|
||||||
"react-redux-loading-bar": "^5.0.8",
|
"react-redux-loading-bar": "^5.0.8",
|
||||||
|
@ -164,7 +163,6 @@
|
||||||
"@types/react-dom": "^18.2.4",
|
"@types/react-dom": "^18.2.4",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.6",
|
||||||
"@types/react-immutable-proptypes": "^2.1.0",
|
"@types/react-immutable-proptypes": "^2.1.0",
|
||||||
"@types/react-motion": "^0.0.40",
|
|
||||||
"@types/react-router": "^5.1.20",
|
"@types/react-router": "^5.1.20",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@types/react-sparklines": "^1.7.2",
|
"@types/react-sparklines": "^1.7.2",
|
||||||
|
|
49
yarn.lock
49
yarn.lock
|
@ -2771,7 +2771,6 @@ __metadata:
|
||||||
"@types/react-dom": "npm:^18.2.4"
|
"@types/react-dom": "npm:^18.2.4"
|
||||||
"@types/react-helmet": "npm:^6.1.6"
|
"@types/react-helmet": "npm:^6.1.6"
|
||||||
"@types/react-immutable-proptypes": "npm:^2.1.0"
|
"@types/react-immutable-proptypes": "npm:^2.1.0"
|
||||||
"@types/react-motion": "npm:^0.0.40"
|
|
||||||
"@types/react-router": "npm:^5.1.20"
|
"@types/react-router": "npm:^5.1.20"
|
||||||
"@types/react-router-dom": "npm:^5.3.3"
|
"@types/react-router-dom": "npm:^5.3.3"
|
||||||
"@types/react-sparklines": "npm:^1.7.2"
|
"@types/react-sparklines": "npm:^1.7.2"
|
||||||
|
@ -2850,7 +2849,6 @@ __metadata:
|
||||||
react-immutable-proptypes: "npm:^2.2.0"
|
react-immutable-proptypes: "npm:^2.2.0"
|
||||||
react-immutable-pure-component: "npm:^2.2.2"
|
react-immutable-pure-component: "npm:^2.2.2"
|
||||||
react-intl: "npm:^7.0.0"
|
react-intl: "npm:^7.0.0"
|
||||||
react-motion: "npm:^0.5.2"
|
|
||||||
react-overlays: "npm:^5.2.1"
|
react-overlays: "npm:^5.2.1"
|
||||||
react-redux: "npm:^9.0.4"
|
react-redux: "npm:^9.0.4"
|
||||||
react-redux-loading-bar: "npm:^5.0.8"
|
react-redux-loading-bar: "npm:^5.0.8"
|
||||||
|
@ -4050,15 +4048,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@types/react-motion@npm:^0.0.40":
|
|
||||||
version: 0.0.40
|
|
||||||
resolution: "@types/react-motion@npm:0.0.40"
|
|
||||||
dependencies:
|
|
||||||
"@types/react": "npm:*"
|
|
||||||
checksum: 10c0/8a560051be917833fdbe051185b53aeafbe8657968ac8e073ac874b9a55c6f16e3793748b13cfb9bd6d9a3d27aba116d6f8f296ec1950f4175dc94d17c5e8470
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@types/react-router-dom@npm:^5.3.3":
|
"@types/react-router-dom@npm:^5.3.3":
|
||||||
version: 5.3.3
|
version: 5.3.3
|
||||||
resolution: "@types/react-router-dom@npm:5.3.3"
|
resolution: "@types/react-router-dom@npm:5.3.3"
|
||||||
|
@ -13189,20 +13178,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"performance-now@npm:^0.2.0":
|
|
||||||
version: 0.2.0
|
|
||||||
resolution: "performance-now@npm:0.2.0"
|
|
||||||
checksum: 10c0/d7f3824e443491208f7124b45d3280dbff889f8f048c3aee507109c24644d51a226eb07fd7ac51dd0eef144639590c57410c2d167bd4fdf0c5caa0101a449c3d
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"performance-now@npm:^2.1.0":
|
|
||||||
version: 2.1.0
|
|
||||||
resolution: "performance-now@npm:2.1.0"
|
|
||||||
checksum: 10c0/22c54de06f269e29f640e0e075207af57de5052a3d15e360c09b9a8663f393f6f45902006c1e71aa8a5a1cdfb1a47fe268826f8496d6425c362f00f5bc3e85d9
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"pg-cloudflare@npm:^1.1.1":
|
"pg-cloudflare@npm:^1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "pg-cloudflare@npm:1.1.1"
|
resolution: "pg-cloudflare@npm:1.1.1"
|
||||||
|
@ -14465,7 +14440,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
"prop-types@npm:^15.5.10, prop-types@npm:^15.5.4, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
|
||||||
version: 15.8.1
|
version: 15.8.1
|
||||||
resolution: "prop-types@npm:15.8.1"
|
resolution: "prop-types@npm:15.8.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -14596,15 +14571,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"raf@npm:^3.1.0":
|
|
||||||
version: 3.4.1
|
|
||||||
resolution: "raf@npm:3.4.1"
|
|
||||||
dependencies:
|
|
||||||
performance-now: "npm:^2.1.0"
|
|
||||||
checksum: 10c0/337f0853c9e6a77647b0f499beedafea5d6facfb9f2d488a624f88b03df2be72b8a0e7f9118a3ff811377d534912039a3311815700d2b6d2313f82f736f9eb6e
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0":
|
"randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0":
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
resolution: "randombytes@npm:2.1.0"
|
resolution: "randombytes@npm:2.1.0"
|
||||||
|
@ -14777,19 +14743,6 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-motion@npm:^0.5.2":
|
|
||||||
version: 0.5.2
|
|
||||||
resolution: "react-motion@npm:0.5.2"
|
|
||||||
dependencies:
|
|
||||||
performance-now: "npm:^0.2.0"
|
|
||||||
prop-types: "npm:^15.5.8"
|
|
||||||
raf: "npm:^3.1.0"
|
|
||||||
peerDependencies:
|
|
||||||
react: ^0.14.9 || ^15.3.0 || ^16.0.0
|
|
||||||
checksum: 10c0/4ea6f1cc7079f0161fd786cc755133a822d87d9c0510369b8fb348d9ad602111efa2e3496dbcc390c967229e39e3eb5f6dd5dd6d3d124289443de31d6035a6c8
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-overlays@npm:^5.2.1":
|
"react-overlays@npm:^5.2.1":
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
resolution: "react-overlays@npm:5.2.1"
|
resolution: "react-overlays@npm:5.2.1"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user