Merge pull request #2567 from ClearlyClaire/glitch-soc/ports/convert-utils-hashtags-typescript

Convert utils and hashtags to Typescript
This commit is contained in:
Claire
2024-01-15 12:41:22 +01:00
committed by GitHub
29 changed files with 284 additions and 265 deletions

View File

@@ -17,7 +17,7 @@ import { Avatar } from './avatar';
import { Button } from './button';
import { FollowersCounter } from './counters';
import { DisplayName } from './display_name';
import Permalink from './permalink';
import { Permalink } from './permalink';
import { RelativeTimestamp } from './relative_timestamp';
const messages = defineMessages({

View File

@@ -6,7 +6,7 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import api from 'flavours/glitch/api';
import Hashtag from 'flavours/glitch/components/hashtag';
import { Hashtag } from 'flavours/glitch/components/hashtag';
export default class Trends extends PureComponent {

View File

@@ -1,123 +0,0 @@
// @ts-check
import PropTypes from 'prop-types';
import { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import Permalink from './permalink';
class SilentErrorBoundary extends Component {
static propTypes = {
children: PropTypes.node,
};
state = {
error: false,
};
componentDidCatch() {
this.setState({ error: true });
}
render() {
if (this.state.error) {
return null;
}
return this.props.children;
}
}
/**
* Used to render counter of how much people are talking about hashtag
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
*/
export const accountsCountRenderer = (displayNumber, pluralReady) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
days: 2,
}}
/>
);
// @ts-expect-error
export const ImmutableHashtag = ({ hashtag }) => (
<Hashtag
name={hashtag.get('name')}
href={hashtag.get('url')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
// @ts-expect-error
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
);
ImmutableHashtag.propTypes = {
hashtag: ImmutablePropTypes.map.isRequired,
};
// @ts-expect-error
const Hashtag = ({ name, href, to, people, uses, history, className, description, withGraph }) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
<Permalink href={href} to={to}>
{name ? <>#<span>{name}</span></> : <Skeleton width={50} />}
</Permalink>
{description ? (
<span>{description}</span>
) : (
typeof people !== 'undefined' ? <ShortNumber value={people} renderer={accountsCountRenderer} /> : <Skeleton width={100} />
)}
</div>
{typeof uses !== 'undefined' && (
<div className='trends__item__current'>
<ShortNumber value={uses} />
</div>
)}
{withGraph && (
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
<Sparklines width={50} height={28} data={history ? history : Array.from(Array(7)).map(() => 0)}>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</SilentErrorBoundary>
</div>
)}
</div>
);
Hashtag.propTypes = {
name: PropTypes.string,
href: PropTypes.string,
to: PropTypes.string,
people: PropTypes.number,
description: PropTypes.node,
uses: PropTypes.number,
history: PropTypes.arrayOf(PropTypes.number),
className: PropTypes.string,
withGraph: PropTypes.bool,
};
Hashtag.defaultProps = {
withGraph: true,
};
export default Hashtag;

View File

@@ -0,0 +1,149 @@
import type { JSX } from 'react';
import { Component } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import type Immutable from 'immutable';
import { Sparklines, SparklinesCurve } from 'react-sparklines';
import { ShortNumber } from 'flavours/glitch/components/short_number';
import { Skeleton } from 'flavours/glitch/components/skeleton';
import { Permalink } from './permalink';
interface SilentErrorBoundaryProps {
children: React.ReactNode;
}
class SilentErrorBoundary extends Component<SilentErrorBoundaryProps> {
state = {
error: false,
};
componentDidCatch() {
this.setState({ error: true });
}
render() {
if (this.state.error) {
return null;
}
return this.props.children;
}
}
/**
* Used to render counter of how much people are talking about hashtag
* @param displayNumber Counter number to display
* @param pluralReady Whether the count is plural
* @returns Formatted counter of how much people are talking about hashtag
*/
export const accountsCountRenderer = (
displayNumber: JSX.Element,
pluralReady: number,
) => (
<FormattedMessage
id='trends.counter_by_accounts'
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} in the past {days, plural, one {day} other {# days}}'
values={{
count: pluralReady,
counter: <strong>{displayNumber}</strong>,
days: 2,
}}
/>
);
interface ImmutableHashtagProps {
hashtag: Immutable.Map<string, unknown>;
}
export const ImmutableHashtag = ({ hashtag }: ImmutableHashtagProps) => (
<Hashtag
name={hashtag.get('name') as string}
href={hashtag.get('url') as string}
to={`/tags/${hashtag.get('name') as string}`}
people={
(hashtag.getIn(['history', 0, 'accounts']) as number) * 1 +
(hashtag.getIn(['history', 1, 'accounts']) as number) * 1
}
history={(
hashtag.get('history') as Immutable.Collection.Indexed<
Immutable.Map<string, number>
>
)
.reverse()
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
.map((day) => day.get('uses')!)
.toArray()}
/>
);
export interface HashtagProps {
className?: string;
description?: React.ReactNode;
history?: number[];
href: string;
name: string;
people: number;
to: string;
uses?: number;
withGraph?: boolean;
}
export const Hashtag: React.FC<HashtagProps> = ({
name,
href,
to,
people,
uses,
history,
className,
description,
withGraph = true,
}) => (
<div className={classNames('trends__item', className)}>
<div className='trends__item__name'>
<Permalink href={href} to={to}>
{name ? (
<>
#<span>{name}</span>
</>
) : (
<Skeleton width={50} />
)}
</Permalink>
{description ? (
<span>{description}</span>
) : typeof people !== 'undefined' ? (
<ShortNumber value={people} renderer={accountsCountRenderer} />
) : (
<Skeleton width={100} />
)}
</div>
{typeof uses !== 'undefined' && (
<div className='trends__item__current'>
<ShortNumber value={uses} />
</div>
)}
{withGraph && (
<div className='trends__item__sparkline'>
<SilentErrorBoundary>
<Sparklines
width={50}
height={28}
data={history ? history : Array.from(Array(7)).map(() => 0)}
>
<SparklinesCurve style={{ fill: 'none' }} />
</Sparklines>
</SilentErrorBoundary>
</div>
)}
</div>
);

View File

@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { withOptionalRouter, WithOptionalRouterPropTypes } from 'flavours/glitch/utils/react_router';
class Permalink extends PureComponent {
static propTypes = {
className: PropTypes.string,
href: PropTypes.string.isRequired,
to: PropTypes.string.isRequired,
children: PropTypes.node,
onInterceptClick: PropTypes.func,
...WithOptionalRouterPropTypes,
};
handleClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
if (this.props.onInterceptClick && this.props.onInterceptClick()) {
e.preventDefault();
return;
}
if (this.props.history) {
e.preventDefault();
this.props.history.push(this.props.to);
}
}
};
render () {
const {
children,
className,
href,
to,
onInterceptClick,
...other
} = this.props;
return (
<a target='_blank' href={href} onClick={this.handleClick} {...other} className={`permalink${className ? ' ' + className : ''}`}>
{children}
</a>
);
}
}
export default withOptionalRouter(Permalink);

View File

@@ -0,0 +1,41 @@
import { useCallback } from 'react';
import { useAppHistory } from './router';
interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
to: string;
}
export const Permalink: React.FC<Props> = ({
className,
href,
to,
children,
...props
}) => {
const history = useAppHistory();
const handleClick = useCallback<React.MouseEventHandler<HTMLAnchorElement>>(
(e) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- history can actually be undefined as the component can be mounted outside a router context
if (e.button === 0 && !(e.ctrlKey || e.metaKey) && history) {
e.preventDefault();
history.push(to);
}
},
[history, to],
);
return (
<a
target='_blank'
rel='noreferrer'
href={href}
onClick={handleClick}
className={`permalink${className ? ' ' + className : ''}`}
{...props}
>
{children}
</a>
);
};

View File

@@ -19,7 +19,7 @@ import { Icon } from 'flavours/glitch/components/icon';
import { autoPlayGif, languages as preloadedLanguages } from 'flavours/glitch/initial_state';
import { decode as decodeIDNA } from 'flavours/glitch/utils/idna';
import Permalink from './permalink';
import { Permalink } from './permalink';
const textMatchesTarget = (text, origin, host) => {
return (text === origin || text === host