Merge pull request #2567 from ClearlyClaire/glitch-soc/ports/convert-utils-hashtags-typescript
Convert utils and hashtags to Typescript
This commit is contained in:
commit
82d28ac956
29 changed files with 284 additions and 265 deletions
|
@ -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({
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
149
app/javascript/flavours/glitch/components/hashtag.tsx
Normal file
149
app/javascript/flavours/glitch/components/hashtag.tsx
Normal 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>
|
||||
);
|
|
@ -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);
|
41
app/javascript/flavours/glitch/components/permalink.tsx
Normal file
41
app/javascript/flavours/glitch/components/permalink.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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
|
||||
|
|
|
@ -5,7 +5,7 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { Hashtag } from 'flavours/glitch/components/hashtag';
|
||||
|
||||
const messages = defineMessages({
|
||||
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' },
|
||||
|
|
|
@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl';
|
|||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { profileLink } from 'flavours/glitch/utils/backend_links';
|
||||
|
||||
import { Avatar } from '../../../components/avatar';
|
||||
|
|
|
@ -15,7 +15,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import AttachmentList from 'flavours/glitch/components/attachment_list';
|
||||
import AvatarComposite from 'flavours/glitch/components/avatar_composite';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp';
|
||||
import StatusContent from 'flavours/glitch/components/status_content';
|
||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
|
|
|
@ -19,7 +19,7 @@ import { Avatar } from 'flavours/glitch/components/avatar';
|
|||
import { Button } from 'flavours/glitch/components/button';
|
||||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { ShortNumber } from 'flavours/glitch/components/short_number';
|
||||
import { autoPlayGif, me, unfollowModal } from 'flavours/glitch/initial_state';
|
||||
import { makeGetAccount } from 'flavours/glitch/selectors';
|
||||
|
|
|
@ -11,7 +11,7 @@ import { ReactComponent as CloseIcon } from '@material-symbols/svg-600/outlined/
|
|||
import { Avatar } from '../../../components/avatar';
|
||||
import { DisplayName } from '../../../components/display_name';
|
||||
import { IconButton } from '../../../components/icon_button';
|
||||
import Permalink from '../../../components/permalink';
|
||||
import { Permalink } from '../../../components/permalink';
|
||||
|
||||
const messages = defineMessages({
|
||||
authorize: { id: 'follow_request.authorize', defaultMessage: 'Authorize' },
|
||||
|
|
|
@ -13,7 +13,7 @@ import { debounce } from 'lodash';
|
|||
|
||||
import { expandFollowedHashtags, fetchFollowedHashtags } from 'flavours/glitch/actions/tags';
|
||||
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||
import Hashtag from 'flavours/glitch/components/hashtag';
|
||||
import { Hashtag } from 'flavours/glitch/components/hashtag';
|
||||
import ScrollableList from 'flavours/glitch/components/scrollable_list';
|
||||
import Column from 'flavours/glitch/features/ui/components/column';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ReactComponent as FlagIcon } from '@material-symbols/svg-600/outlined/f
|
|||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import NotificationOverlayContainer from '../containers/overlay_container';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outli
|
|||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { ReactComponent as PersonAddIcon } from '@material-symbols/svg-600/outli
|
|||
import { HotKeys } from 'react-hotkeys';
|
||||
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import AccountContainer from 'flavours/glitch/containers/account_container';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import { Avatar } from 'flavours/glitch/components/avatar';
|
|||
import { DisplayName } from 'flavours/glitch/components/display_name';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { IconButton } from 'flavours/glitch/components/icon_button';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
||||
import NotificationOverlayContainer from '../containers/overlay_container';
|
||||
|
|
|
@ -14,7 +14,7 @@ import { fetchServer } from 'flavours/glitch/actions/server';
|
|||
import { Avatar } from 'flavours/glitch/components/avatar';
|
||||
import { Icon } from 'flavours/glitch/components/icon';
|
||||
import { WordmarkLogo, SymbolLogo } from 'flavours/glitch/components/logo';
|
||||
import Permalink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import { registrationsOpen, me, sso_redirect } from 'flavours/glitch/initial_state';
|
||||
|
||||
const Account = connect(state => ({
|
||||
|
|
|
@ -15,7 +15,7 @@ import { HotKeys } from 'react-hotkeys';
|
|||
import { changeLayout } from 'flavours/glitch/actions/app';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'flavours/glitch/actions/markers';
|
||||
import { INTRODUCTION_VERSION } from 'flavours/glitch/actions/onboarding';
|
||||
import PermaLink from 'flavours/glitch/components/permalink';
|
||||
import { Permalink } from 'flavours/glitch/components/permalink';
|
||||
import PictureInPicture from 'flavours/glitch/features/picture_in_picture';
|
||||
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||
import { WithRouterPropTypes } from 'flavours/glitch/utils/react_router';
|
||||
|
@ -649,9 +649,9 @@ class UI extends PureComponent {
|
|||
id='moved_to_warning'
|
||||
defaultMessage='This account is marked as moved to {moved_to_link}, and may thus not accept new follows.'
|
||||
values={{ moved_to_link: (
|
||||
<PermaLink href={moved.get('url')} to={`/@${moved.get('acct')}`}>
|
||||
<Permalink href={moved.get('url')} to={`/@${moved.get('acct')}`}>
|
||||
@{moved.get('acct')}
|
||||
</PermaLink>
|
||||
</Permalink>
|
||||
) }}
|
||||
/>
|
||||
</div>)}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
import ready from '../ready';
|
||||
|
||||
export let assetHost = '';
|
||||
|
||||
ready(() => {
|
||||
const cdnHost = document.querySelector('meta[name=cdn-host]');
|
||||
if (cdnHost) {
|
||||
assetHost = cdnHost.content || '';
|
||||
}
|
||||
});
|
13
app/javascript/flavours/glitch/utils/config.ts
Normal file
13
app/javascript/flavours/glitch/utils/config.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import ready from '../ready';
|
||||
|
||||
export let assetHost = '';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
ready(() => {
|
||||
const cdnHost = document.querySelector<HTMLMetaElement>(
|
||||
'meta[name=cdn-host]',
|
||||
);
|
||||
if (cdnHost) {
|
||||
assetHost = cdnHost.content || '';
|
||||
}
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
// NB: This function can still return unsafe HTML
|
||||
export const unescapeHTML = (html) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html.replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n').replace(/<[^>]*>/g, '');
|
||||
return wrapper.textContent;
|
||||
};
|
9
app/javascript/flavours/glitch/utils/html.ts
Normal file
9
app/javascript/flavours/glitch/utils/html.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
// NB: This function can still return unsafe HTML
|
||||
export const unescapeHTML = (html: string) => {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.innerHTML = html
|
||||
.replace(/<br\s*\/?>/g, '\n')
|
||||
.replace(/<\/p><p>/g, '\n\n')
|
||||
.replace(/<[^>]*>/g, '');
|
||||
return wrapper.textContent;
|
||||
};
|
|
@ -1,13 +1,23 @@
|
|||
// Copied from emoji-mart for consistency with emoji picker and since
|
||||
// they don't export the icons in the package
|
||||
export const loupeIcon = (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 20 20'
|
||||
width='13'
|
||||
height='13'
|
||||
>
|
||||
<path d='M12.9 14.32a8 8 0 1 1 1.41-1.41l5.35 5.33-1.42 1.42-5.33-5.34zM8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12z' />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const deleteIcon = (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' width='13' height='13'>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
viewBox='0 0 20 20'
|
||||
width='13'
|
||||
height='13'
|
||||
>
|
||||
<path d='M10 8.586L2.929 1.515 1.515 2.929 8.586 10l-7.071 7.071 1.414 1.414L10 11.414l7.071 7.071 1.414-1.414L11.414 10l7.071-7.071-1.414-1.414L10 8.586z' />
|
||||
</svg>
|
||||
);
|
|
@ -1,30 +0,0 @@
|
|||
// Handles browser quirks, based on
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
|
||||
|
||||
const checkNotificationPromise = () => {
|
||||
try {
|
||||
// eslint-disable-next-line promise/valid-params, promise/catch-or-return
|
||||
Notification.requestPermission().then();
|
||||
} catch(e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handlePermission = (permission, callback) => {
|
||||
// Whatever the user answers, we make sure Chrome stores the information
|
||||
if(!('permission' in Notification)) {
|
||||
Notification.permission = permission;
|
||||
}
|
||||
|
||||
callback(Notification.permission);
|
||||
};
|
||||
|
||||
export const requestNotificationPermission = (callback) => {
|
||||
if (checkNotificationPromise()) {
|
||||
Notification.requestPermission().then((permission) => handlePermission(permission, callback)).catch(console.warn);
|
||||
} else {
|
||||
Notification.requestPermission((permission) => handlePermission(permission, callback));
|
||||
}
|
||||
};
|
13
app/javascript/flavours/glitch/utils/notifications.ts
Normal file
13
app/javascript/flavours/glitch/utils/notifications.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* Tries Notification.requestPermission, console warning instead of rejecting on error.
|
||||
* @param callback Runs with the permission result on completion.
|
||||
*/
|
||||
export const requestNotificationPermission = async (
|
||||
callback: NotificationPermissionCallback,
|
||||
) => {
|
||||
try {
|
||||
callback(await Notification.requestPermission());
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
|
@ -1,8 +1,8 @@
|
|||
import PropTypes from "prop-types";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { __RouterContext } from "react-router";
|
||||
import { __RouterContext } from 'react-router';
|
||||
|
||||
import hoistStatics from "hoist-non-react-statics";
|
||||
import hoistStatics from 'hoist-non-react-statics';
|
||||
|
||||
export const WithRouterPropTypes = {
|
||||
match: PropTypes.object.isRequired,
|
||||
|
@ -16,31 +16,37 @@ export const WithOptionalRouterPropTypes = {
|
|||
history: PropTypes.object,
|
||||
};
|
||||
|
||||
export interface OptionalRouterProps {
|
||||
ref: unknown;
|
||||
wrappedComponentRef: unknown;
|
||||
}
|
||||
|
||||
// This is copied from https://github.com/remix-run/react-router/blob/v5.3.4/packages/react-router/modules/withRouter.js
|
||||
// but does not fail if called outside of a React Router context
|
||||
export function withOptionalRouter(Component) {
|
||||
const displayName = `withRouter(${Component.displayName || Component.name})`;
|
||||
const C = props => {
|
||||
export function withOptionalRouter<
|
||||
ComponentType extends React.ComponentType<OptionalRouterProps>,
|
||||
>(Component: ComponentType) {
|
||||
const displayName = `withRouter(${Component.displayName ?? Component.name})`;
|
||||
const C = (props: React.ComponentProps<ComponentType>) => {
|
||||
const { wrappedComponentRef, ...remainingProps } = props;
|
||||
|
||||
return (
|
||||
<__RouterContext.Consumer>
|
||||
{context => {
|
||||
if(context)
|
||||
{(context) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (context) {
|
||||
return (
|
||||
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||
<Component
|
||||
{...remainingProps}
|
||||
{...context}
|
||||
ref={wrappedComponentRef}
|
||||
/>
|
||||
);
|
||||
else
|
||||
return (
|
||||
<Component
|
||||
{...remainingProps}
|
||||
ref={wrappedComponentRef}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// @ts-expect-error - Dynamic covariant generic components are tough to type.
|
||||
return <Component {...remainingProps} ref={wrappedComponentRef} />;
|
||||
}
|
||||
}}
|
||||
</__RouterContext.Consumer>
|
||||
);
|
||||
|
@ -53,8 +59,8 @@ export function withOptionalRouter(Component) {
|
|||
wrappedComponentRef: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.func,
|
||||
PropTypes.object
|
||||
])
|
||||
PropTypes.object,
|
||||
]),
|
||||
};
|
||||
|
||||
return hoistStatics(C, Component);
|
|
@ -1,9 +1,7 @@
|
|||
/** @type {number | null} */
|
||||
let cachedScrollbarWidth = null;
|
||||
import { isMobile } from '../is_mobile';
|
||||
|
||||
let cachedScrollbarWidth: number | null = null;
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
const getActualScrollbarWidth = () => {
|
||||
const outer = document.createElement('div');
|
||||
outer.style.visibility = 'hidden';
|
||||
|
@ -14,20 +12,19 @@ const getActualScrollbarWidth = () => {
|
|||
outer.appendChild(inner);
|
||||
|
||||
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
||||
outer.parentNode.removeChild(outer);
|
||||
outer.remove();
|
||||
|
||||
return scrollbarWidth;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {number}
|
||||
*/
|
||||
export const getScrollbarWidth = () => {
|
||||
if (cachedScrollbarWidth !== null) {
|
||||
return cachedScrollbarWidth;
|
||||
}
|
||||
|
||||
const scrollbarWidth = getActualScrollbarWidth();
|
||||
const scrollbarWidth = isMobile(window.innerWidth)
|
||||
? 0
|
||||
: getActualScrollbarWidth();
|
||||
cachedScrollbarWidth = scrollbarWidth;
|
||||
|
||||
return scrollbarWidth;
|
Loading…
Reference in a new issue