update Preact to Preact X
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
'markdown-toolbar': {
|
||||
for: string;
|
||||
children?: any;
|
||||
className: string;
|
||||
};
|
||||
'md-bold': any;
|
||||
'md-header': any;
|
||||
'md-italic': any;
|
||||
'md-quote': any;
|
||||
'md-code': any;
|
||||
'md-link': any;
|
||||
'md-unordered-list': any;
|
||||
'md-ordered-list': any;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import * as preact from 'preact/src/jsx';
|
||||
|
||||
declare module 'preact/src/jsx' {
|
||||
namespace JSXInternal {
|
||||
interface IntrinsicElements {
|
||||
'markdown-toolbar': {
|
||||
for: string;
|
||||
children?: any;
|
||||
className: string;
|
||||
};
|
||||
'md-bold': any;
|
||||
'md-header': any;
|
||||
'md-italic': any;
|
||||
'md-quote': any;
|
||||
'md-code': any;
|
||||
'md-link': any;
|
||||
'md-unordered-list': any;
|
||||
'md-ordered-list': any;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
@@ -17,7 +17,7 @@ interface State {
|
||||
export class AnonymousLoginForm extends Component<Props, State> {
|
||||
static usernameRegex = /^[a-zA-Z][\w ]+$/;
|
||||
|
||||
inputRef?: HTMLInputElement;
|
||||
inputRef = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -60,11 +60,12 @@ export class AnonymousLoginForm extends Component<Props, State> {
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
this.inputRef.current && this.inputRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>) {
|
||||
render() {
|
||||
const props = this.props;
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-panel-anonymous-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
@@ -77,7 +78,7 @@ export class AnonymousLoginForm extends Component<Props, State> {
|
||||
<form className={className} onSubmit={this.onSubmit}>
|
||||
<input
|
||||
className="auth-panel-anonymous-login-form__input"
|
||||
ref={ref => (this.inputRef = ref)}
|
||||
ref={this.inputRef}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.inputValue}
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { EmailLoginForm, Props, State } from './auth-panel__email-login-form';
|
||||
import { User } from '@app/common/types';
|
||||
|
||||
+27
-24
@@ -1,10 +1,11 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import b from 'bem-react-helper';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Theme, User } from '@app/common/types';
|
||||
import { sendEmailVerificationRequest } from '@app/common/api';
|
||||
import { extractErrorMessageFromResponse } from '@app/utils/errorUtils';
|
||||
import { connect } from 'preact-redux';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
import TextareaAutosize from '@app/components/input/textarea-autosize';
|
||||
@@ -13,12 +14,14 @@ const mapStateToProps = () => ({
|
||||
sendEmailVerification: sendEmailVerificationRequest,
|
||||
});
|
||||
|
||||
export type Props = {
|
||||
interface OwnProps {
|
||||
onSignIn(token: string): Promise<User | null>;
|
||||
onSuccess?(user: User): Promise<void>;
|
||||
theme: Theme;
|
||||
className?: string;
|
||||
} & ReturnType<typeof mapStateToProps>;
|
||||
}
|
||||
|
||||
export type Props = OwnProps & ReturnType<typeof mapStateToProps>;
|
||||
|
||||
export interface State {
|
||||
usernameValue: string;
|
||||
@@ -33,8 +36,8 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
static usernameRegex = /^[a-zA-Z][\w ]+$/;
|
||||
static emailRegex = /[^@]+@[^.]+\..+/;
|
||||
|
||||
inputRef?: HTMLInputElement;
|
||||
tokenRef?: TextareaAutosize;
|
||||
inputRef = createRef<HTMLInputElement>();
|
||||
tokenRef = createRef<TextareaAutosize>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -59,11 +62,11 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
|
||||
async focus() {
|
||||
await sleep(100);
|
||||
if (this.inputRef) {
|
||||
this.inputRef.focus();
|
||||
if (this.inputRef.current) {
|
||||
this.inputRef.current.focus();
|
||||
return;
|
||||
}
|
||||
this.tokenRef && this.tokenRef.textareaRef && this.tokenRef.textareaRef.select();
|
||||
this.tokenRef.current && this.tokenRef.current.textareaRef && this.tokenRef.current.textareaRef.select();
|
||||
}
|
||||
|
||||
async onVerificationSubmit(e: Event) {
|
||||
@@ -73,7 +76,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
await this.props.sendEmailVerification(this.state.usernameValue, this.state.addressValue);
|
||||
this.setState({ verificationSent: true });
|
||||
setTimeout(() => {
|
||||
this.tokenRef && this.tokenRef.focus();
|
||||
this.tokenRef.current && this.tokenRef.current.focus();
|
||||
}, 100);
|
||||
} catch (e) {
|
||||
this.setState({ error: extractErrorMessageFromResponse(e) });
|
||||
@@ -119,7 +122,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
verificationSent: false,
|
||||
});
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
this.inputRef.current && this.inputRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -141,11 +144,11 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
this.inputRef.current && this.inputRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>) {
|
||||
render(props: Props) {
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-panel-email-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
@@ -173,7 +176,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
</span>
|
||||
<input
|
||||
className="auth-panel-email-login-form__input"
|
||||
ref={ref => (this.inputRef = ref)}
|
||||
ref={this.inputRef}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.usernameValue}
|
||||
@@ -193,7 +196,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
title={form1InvalidReason || ''}
|
||||
disabled={form1InvalidReason !== null}
|
||||
/>
|
||||
{this.state.error && <div class="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -207,7 +210,7 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
<TextareaAutosize
|
||||
autofocus={true}
|
||||
className="auth-panel-email-login-form__token-input"
|
||||
ref={ref => (this.tokenRef = ref)}
|
||||
ref={this.tokenRef}
|
||||
placeholder="Token"
|
||||
value={this.state.tokenValue}
|
||||
onInput={this.onTokenChange}
|
||||
@@ -221,15 +224,15 @@ export class EmailLoginForm extends Component<Props, State> {
|
||||
title={form2InvalidReason || ''}
|
||||
disabled={form2InvalidReason !== null}
|
||||
/>
|
||||
{this.state.error && <div class="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
{this.state.error && <div className="auth-panel-email-login-form__error">{this.state.error}</div>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const EmailLoginFormConnected = connect(
|
||||
mapStateToProps,
|
||||
null,
|
||||
null,
|
||||
{ withRef: true }
|
||||
)(EmailLoginForm);
|
||||
export type EmailLoginFormRef = EmailLoginForm;
|
||||
|
||||
export const EmailLoginFormConnected = forwardRef<EmailLoginForm, OwnProps>((props, ref) => {
|
||||
const connectedProps = useSelector(mapStateToProps);
|
||||
return <EmailLoginForm {...props} {...connectedProps} ref={ref} />;
|
||||
});
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, FunctionComponent } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
import { exclude } from '@app/utils/exclude';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
id: string;
|
||||
theme: Theme;
|
||||
}
|
||||
} & JSX.HTMLAttributes;
|
||||
|
||||
export const UserID = (props: JSX.HTMLAttributes & Props) => (
|
||||
<div
|
||||
{...exclude(props, 'id', 'theme')}
|
||||
className={b('auth-panel__user-id', {}, { theme: props.theme })}
|
||||
title={props.id}
|
||||
>
|
||||
{props.id}
|
||||
export const UserID: FunctionComponent<Props> = ({ id, theme, ...props }) => (
|
||||
<div {...props} className={b('auth-panel__user-id', {}, { theme })} title={id}>
|
||||
{id}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { Props, AuthPanel } from './auth-panel';
|
||||
import { User, PostInfo } from '../../common/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants';
|
||||
@@ -11,11 +11,12 @@ import Dropdown, { DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import { UserID } from './__user-id';
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
import { EmailLoginForm, EmailLoginFormConnected } from './__email-login-form';
|
||||
import { EmailLoginFormConnected } from './__email-login-form';
|
||||
import { StoreState } from '@app/store';
|
||||
import { ProviderState } from '@app/store/provider/reducers';
|
||||
import debounce from '@app/utils/debounce';
|
||||
import postMessage from '@app/utils/postMessage';
|
||||
import { EmailLoginFormRef } from './__email-login-form/auth-panel__email-login-form';
|
||||
|
||||
export interface Props {
|
||||
user: User | null;
|
||||
@@ -44,7 +45,7 @@ interface State {
|
||||
}
|
||||
|
||||
export class AuthPanel extends Component<Props, State> {
|
||||
emailLoginRef?: EmailLoginForm;
|
||||
emailLoginRef = createRef<EmailLoginFormRef>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -85,7 +86,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
}, 100);
|
||||
|
||||
onEmailTitleClick() {
|
||||
this.emailLoginRef && this.emailLoginRef.focus();
|
||||
this.emailLoginRef.current && this.emailLoginRef.current.focus();
|
||||
}
|
||||
|
||||
onSortChange(e: Event) {
|
||||
@@ -208,7 +209,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
>
|
||||
<DropdownItem>
|
||||
<EmailLoginFormConnected
|
||||
ref={ref => (this.emailLoginRef = ref ? ref.getWrappedInstance() : null)}
|
||||
ref={this.emailLoginRef}
|
||||
onSignIn={this.onEmailSignIn}
|
||||
theme={this.props.theme}
|
||||
className="auth-panel__email-login-form"
|
||||
@@ -305,7 +306,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
<div className="auth-panel__column">
|
||||
Disable third-party cookies blocking to sign in or open comments in{' '}
|
||||
<a
|
||||
class="auth-panel__pseudo-link"
|
||||
className="auth-panel__pseudo-link"
|
||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||
target="_blank"
|
||||
>
|
||||
@@ -373,7 +374,7 @@ export class AuthPanel extends Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
render(props: RenderableProps<Props>, { isBlockedVisible }: State) {
|
||||
render(props: Props, { isBlockedVisible }: State) {
|
||||
const {
|
||||
user,
|
||||
postInfo: { read_only },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/** @jsx h */
|
||||
import { Component, h, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, Component } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import noop from '@app/utils/noop';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
type?: string;
|
||||
kind?: string;
|
||||
theme: Theme;
|
||||
@@ -14,14 +14,14 @@ interface Props {
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
onFocus?: (e: FocusEvent) => void;
|
||||
onBlur?: (e: FocusEvent) => void;
|
||||
}
|
||||
} & JSX.HTMLAttributes;
|
||||
|
||||
interface State {
|
||||
isClicked: boolean;
|
||||
isFocused: boolean;
|
||||
}
|
||||
|
||||
export class Button extends Component<JSX.HTMLAttributes & Props, State> {
|
||||
export class Button extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -62,7 +62,7 @@ export class Button extends Component<JSX.HTMLAttributes & Props, State> {
|
||||
this.props.onFocus!(e);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<JSX.HTMLAttributes & Props>, state: State) {
|
||||
render(props: Props, state: State) {
|
||||
const { children, className } = props;
|
||||
const { isClicked, isFocused } = state;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import { Props, Comment } from './comment';
|
||||
import { User, Comment as CommentType, PostInfo } from '@app/common/types';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/** @jsx h */
|
||||
/** @jsx createElement */
|
||||
|
||||
import './styles';
|
||||
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import { createElement, JSX, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
@@ -72,7 +72,7 @@ export interface State {
|
||||
export class Comment extends Component<Props, State> {
|
||||
votingPromise: Promise<unknown>;
|
||||
/** comment text node. Used in comment text copying */
|
||||
textNode?: HTMLDivElement;
|
||||
textNode = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -305,7 +305,7 @@ export class Comment extends Component<Props, State> {
|
||||
copyComment = () => {
|
||||
const username = this.props.data.user.name;
|
||||
const time = this.props.data.time;
|
||||
const text = this.textNode!.textContent || '';
|
||||
const text = this.textNode.current!.textContent || '';
|
||||
|
||||
copy(`<b>${username}</b> ${time}<br>${text.replace(/\n+/g, '<br>')}`);
|
||||
|
||||
@@ -447,7 +447,7 @@ export class Comment extends Component<Props, State> {
|
||||
return controls;
|
||||
};
|
||||
|
||||
render(props: RenderableProps<Props>, state: State) {
|
||||
render(props: Props, state: State) {
|
||||
const isAdmin = this.isAdmin();
|
||||
const isGuest = this.isGuest();
|
||||
const isCurrentUser = this.isCurrentUser();
|
||||
@@ -547,7 +547,9 @@ export class Comment extends Component<Props, State> {
|
||||
}
|
||||
|
||||
if (!props.editMode && this.props.inView === false) {
|
||||
const [width, height] = this.base ? [this.base.scrollWidth, this.base.scrollHeight] : [100, 100];
|
||||
const [width, height] = this.base
|
||||
? [(this.base as Element).scrollWidth, (this.base as Element).scrollHeight]
|
||||
: [100, 100];
|
||||
return (
|
||||
<article
|
||||
id={props.disabled ? undefined : `${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}
|
||||
@@ -670,7 +672,7 @@ export class Comment extends Component<Props, State> {
|
||||
{(!props.collapsed || props.view === 'pinned') && (
|
||||
<div
|
||||
className={b('comment__text', { mix: b('raw-content', {}, { theme: props.theme }) })}
|
||||
ref={r => (this.textNode = r)}
|
||||
ref={this.textNode}
|
||||
dangerouslySetInnerHTML={{ __html: o.text }}
|
||||
/>
|
||||
)}
|
||||
|
||||
+27
-20
@@ -3,11 +3,15 @@
|
||||
* and should be importded explicitly
|
||||
*/
|
||||
|
||||
/** @jsx createElement */
|
||||
|
||||
import './styles';
|
||||
|
||||
import { createElement, FunctionComponent } from 'preact';
|
||||
|
||||
import { Comment as CommentType } from '@app/common/types';
|
||||
|
||||
import { connect } from 'preact-redux';
|
||||
import { useStore } from 'react-redux';
|
||||
|
||||
import { StoreState } from '@app/store';
|
||||
import {
|
||||
@@ -26,27 +30,30 @@ import { getCommentMode } from '@app/store/comments/getters';
|
||||
import { uploadImage, getPreview } from '@app/common/api';
|
||||
import { getThreadIsCollapsed } from '@app/store/thread/getters';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
import { useActions } from '@app/hooks/useAction';
|
||||
|
||||
type ProvidedProps = Pick<
|
||||
Props,
|
||||
| 'editMode'
|
||||
| 'user'
|
||||
| 'isUserBanned'
|
||||
| 'post_info'
|
||||
| 'isCommentsDisabled'
|
||||
| 'theme'
|
||||
| 'collapsed'
|
||||
| 'getPreview'
|
||||
| 'uploadImage'
|
||||
>;
|
||||
|
||||
const mapStateToProps = (state: StoreState, cprops: { data: CommentType }) => {
|
||||
const props: Pick<
|
||||
Props,
|
||||
| 'editMode'
|
||||
| 'user'
|
||||
| 'isUserBanned'
|
||||
| 'post_info'
|
||||
| 'isCommentsDisabled'
|
||||
| 'theme'
|
||||
| 'collapsed'
|
||||
| 'getPreview'
|
||||
| 'uploadImage'
|
||||
> = {
|
||||
editMode: getCommentMode(state, cprops.data.id),
|
||||
const props: ProvidedProps = {
|
||||
editMode: getCommentMode(cprops.data.id)(state),
|
||||
user: state.user,
|
||||
isUserBanned: cprops.data.user.block || state.bannedUsers.find(u => u.id === cprops.data.user.id) !== undefined,
|
||||
post_info: state.info,
|
||||
isCommentsDisabled: state.info.read_only || false,
|
||||
theme: state.theme,
|
||||
collapsed: getThreadIsCollapsed(state, cprops.data),
|
||||
collapsed: getThreadIsCollapsed(cprops.data)(state),
|
||||
getPreview,
|
||||
uploadImage,
|
||||
};
|
||||
@@ -67,8 +74,8 @@ export const boundActions = bindActions({
|
||||
setVerifyStatus: setVerifiedStatus,
|
||||
});
|
||||
|
||||
/** Comment component connected to redux */
|
||||
export const ConnectedComment = connect(
|
||||
mapStateToProps,
|
||||
boundActions as Partial<typeof boundActions>
|
||||
)(Comment);
|
||||
export const ConnectedComment: FunctionComponent<Omit<Props, keyof (ProvidedProps & typeof bindActions)>> = props => {
|
||||
const providedProps = mapStateToProps(useStore().getState(), props);
|
||||
const actions = useActions(boundActions);
|
||||
return <Comment {...props} {...providedProps} {...actions} />;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, Component, createRef } from 'preact';
|
||||
import { exclude } from '@app/utils/exclude';
|
||||
|
||||
type Props = {
|
||||
@@ -14,7 +14,7 @@ interface State {
|
||||
|
||||
/** Component which uses plain DOM mutation instead of rerendering react reactive reactivity */
|
||||
export default class Countdown extends Component<Props, State> {
|
||||
elemRef?: HTMLSpanElement;
|
||||
elemRef = createRef<HTMLSpanElement>();
|
||||
intervalID?: number;
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -41,7 +41,7 @@ export default class Countdown extends Component<Props, State> {
|
||||
tick() {
|
||||
if (this.elemRef) {
|
||||
const value = Math.max(0, (this.state.time - new Date().getTime()) / 1000).toFixed(0);
|
||||
this.elemRef!.innerText = value;
|
||||
this.elemRef.current!.innerText = value;
|
||||
if (value === '0') {
|
||||
this.props.onTimePassed && this.props.onTimePassed();
|
||||
window.clearInterval(this.intervalID);
|
||||
@@ -56,7 +56,7 @@ export default class Countdown extends Component<Props, State> {
|
||||
this.tick();
|
||||
}, 1000);
|
||||
}
|
||||
render(props: RenderableProps<Props>) {
|
||||
return <span {...exclude(props, 'time', 'onTimePassed')} ref={ref => (this.elemRef = ref)} />;
|
||||
render(props: Props) {
|
||||
return <span {...exclude(props, 'time', 'onTimePassed')} ref={this.elemRef} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { Component, h, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { Button } from '@app/components/button';
|
||||
@@ -24,7 +24,7 @@ interface State {
|
||||
}
|
||||
|
||||
export default class Dropdown extends Component<Props, State> {
|
||||
rootNode?: HTMLDivElement;
|
||||
rootNode = createRef<HTMLDivElement>();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@@ -52,10 +52,10 @@ export default class Dropdown extends Component<Props, State> {
|
||||
await this.__adjustDropDownContent();
|
||||
if (isActive) {
|
||||
this.__onOpen();
|
||||
this.props.onOpen && this.props.onOpen(this.rootNode!);
|
||||
this.props.onOpen && this.props.onOpen(this.rootNode.current!);
|
||||
} else {
|
||||
this.__onClose();
|
||||
this.props.onClose && this.props.onClose(this.rootNode!);
|
||||
this.props.onClose && this.props.onClose(this.rootNode.current!);
|
||||
}
|
||||
|
||||
if (this.props.onTitleClick) {
|
||||
@@ -71,8 +71,8 @@ export default class Dropdown extends Component<Props, State> {
|
||||
|
||||
__onOpen() {
|
||||
const isChildOfDropDown = (() => {
|
||||
if (!this.rootNode) return false;
|
||||
let parent = this.rootNode.parentElement!;
|
||||
if (!this.rootNode.current) return false;
|
||||
let parent = this.rootNode.current.parentElement!;
|
||||
while (parent !== document.body) {
|
||||
if (parent.classList.contains('dropdown')) return true;
|
||||
parent = parent.parentElement!;
|
||||
@@ -87,10 +87,10 @@ export default class Dropdown extends Component<Props, State> {
|
||||
let prevDcBottom: number | null = null;
|
||||
|
||||
this.checkInterval = window.setInterval(() => {
|
||||
if (!this.rootNode || !this.state.isActive) return;
|
||||
if (!this.rootNode.current || !this.state.isActive) return;
|
||||
const windowHeight = window.innerHeight;
|
||||
const dcBottom = (() => {
|
||||
const dc = Array.from(this.rootNode.children).find(c => c.classList.contains('dropdown__content'));
|
||||
const dc = Array.from(this.rootNode.current.children).find(c => c.classList.contains('dropdown__content'));
|
||||
if (!dc) return 0;
|
||||
const rect = dc.getBoundingClientRect();
|
||||
return window.scrollY + Math.abs(rect.top) + dc.scrollHeight + 10;
|
||||
@@ -111,8 +111,8 @@ export default class Dropdown extends Component<Props, State> {
|
||||
}
|
||||
|
||||
async __adjustDropDownContent() {
|
||||
if (!this.rootNode) return;
|
||||
const dc = this.rootNode.querySelector<HTMLDivElement>('.dropdown__content');
|
||||
if (!this.rootNode.current) return;
|
||||
const dc = this.rootNode.current.querySelector<HTMLDivElement>('.dropdown__content');
|
||||
if (!dc) return;
|
||||
await sleep(10);
|
||||
const rect = dc.getBoundingClientRect();
|
||||
@@ -144,14 +144,14 @@ export default class Dropdown extends Component<Props, State> {
|
||||
},
|
||||
() => {
|
||||
this.__onClose();
|
||||
this.props.onClose && this.props.onClose(this.rootNode!);
|
||||
this.props.onClose && this.props.onClose(this.rootNode.current!);
|
||||
}
|
||||
);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onOutsideClick(e: MouseEvent) {
|
||||
if (!this.rootNode || this.rootNode.contains(e.target as Node) || !this.state.isActive) return;
|
||||
if (!this.rootNode.current || this.rootNode.current.contains(e.target as Node) || !this.state.isActive) return;
|
||||
this.setState(
|
||||
{
|
||||
contentTranslateX: 0,
|
||||
@@ -159,7 +159,7 @@ export default class Dropdown extends Component<Props, State> {
|
||||
},
|
||||
() => {
|
||||
this.__onClose();
|
||||
this.props.onClose && this.props.onClose(this.rootNode!);
|
||||
this.props.onClose && this.props.onClose(this.rootNode.current!);
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -176,11 +176,12 @@ export default class Dropdown extends Component<Props, State> {
|
||||
window.removeEventListener('message', this.receiveMessage);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, { isActive }: State) {
|
||||
const { title, titleClass, heading, children, mix } = props;
|
||||
render() {
|
||||
const { title, titleClass, heading, children, mix, theme } = this.props;
|
||||
const { isActive } = this.state;
|
||||
|
||||
return (
|
||||
<div className={b('dropdown', { mix }, { theme: props.theme, active: isActive })} ref={r => (this.rootNode = r)}>
|
||||
<div className={b('dropdown', { mix }, { theme, active: isActive })} ref={this.rootNode}>
|
||||
<Button
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isActive && 'true'}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/** @jsx h */
|
||||
/** @jsx createElement */
|
||||
|
||||
/* styles imports */
|
||||
import '@app/components/raw-content';
|
||||
import './styles';
|
||||
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import { createElement, Component, createRef } from 'preact';
|
||||
import b, { Mix } from 'bem-react-helper';
|
||||
|
||||
import { User, Theme, Image, ApiError } from '@app/common/types';
|
||||
@@ -66,8 +66,9 @@ const ImageMimeRegex = /image\//i;
|
||||
|
||||
export class Input extends Component<Props, State> {
|
||||
/** reference to textarea element */
|
||||
textAreaRef?: TextareaAutosize;
|
||||
textAreaRef = createRef<TextareaAutosize>();
|
||||
textareaId: string;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
textareaId = textareaId + 1;
|
||||
@@ -98,7 +99,7 @@ export class Input extends Component<Props, State> {
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
if (nextProps.value !== this.props.value) {
|
||||
this.setState({ text: nextProps.value || '' });
|
||||
this.props.autofocus && this.textAreaRef && this.textAreaRef.focus();
|
||||
this.props.autofocus && this.textAreaRef.current && this.textAreaRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,14 +243,14 @@ export class Input extends Component<Props, State> {
|
||||
/** performs upload process */
|
||||
async uploadImages(files: File[]) {
|
||||
if (!this.props.uploadImage) return;
|
||||
if (!this.textAreaRef) return;
|
||||
if (!this.textAreaRef.current) return;
|
||||
|
||||
/** Human readable image size limit, i.e 5MB */
|
||||
const maxImageSizeString = (StaticStore.config.max_image_size / 1024 / 1024).toFixed(2) + 'MB';
|
||||
/** upload delay to avoid server rate limiter */
|
||||
const uploadDelay = 5000;
|
||||
|
||||
const isSelectionSupported = this.textAreaRef.isSelectionSupported();
|
||||
const isSelectionSupported = this.textAreaRef.current.isSelectionSupported();
|
||||
|
||||
this.setState({
|
||||
errorLock: true,
|
||||
@@ -297,7 +298,7 @@ export class Input extends Component<Props, State> {
|
||||
|
||||
const uploadPlaceholder = `${placeholderStart}![uploading ${file.name}...]()`;
|
||||
const uploadPlaceholderLength = uploadPlaceholder.length;
|
||||
const selection = this.textAreaRef.getSelection();
|
||||
const selection = this.textAreaRef.current.getSelection();
|
||||
/** saved selection in case of error */
|
||||
const originalText = this.state.text;
|
||||
const restoreSelection = async () => {
|
||||
@@ -306,7 +307,7 @@ export class Input extends Component<Props, State> {
|
||||
});
|
||||
/** sleeping awhile so textarea catch state change and its selection */
|
||||
await sleep(100);
|
||||
this.textAreaRef!.setSelection(selection);
|
||||
this.textAreaRef.current!.setSelection(selection);
|
||||
};
|
||||
|
||||
if (file.size > StaticStore.config.max_image_size) {
|
||||
@@ -335,16 +336,13 @@ export class Input extends Component<Props, State> {
|
||||
/** sleeping awhile so textarea catch state change and its selection */
|
||||
await sleep(100);
|
||||
const selectionPointer = selection[0] + markdownString.length;
|
||||
this.textAreaRef.setSelection([selectionPointer, selectionPointer]);
|
||||
this.textAreaRef.current.setSelection([selectionPointer, selectionPointer]);
|
||||
}
|
||||
|
||||
this.setState({ errorLock: false, isDisabled: false, buttonText: null });
|
||||
}
|
||||
|
||||
render(
|
||||
props: RenderableProps<Props>,
|
||||
{ isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State
|
||||
) {
|
||||
render(props: Props, { isDisabled, isErrorShown, errorMessage, preview, maxLength, text, buttonText }: State) {
|
||||
const charactersLeft = maxLength - text.length;
|
||||
errorMessage = props.errorMessage || errorMessage;
|
||||
const label = buttonText || Labels[props.mode || 'main'];
|
||||
@@ -374,7 +372,7 @@ export class Input extends Component<Props, State> {
|
||||
<TextareaAutosize
|
||||
id={this.textareaId}
|
||||
onPaste={this.onPaste}
|
||||
ref={ref => (this.textAreaRef = ref)}
|
||||
ref={this.textAreaRef}
|
||||
className="input__field"
|
||||
placeholder="Your comment here"
|
||||
value={text}
|
||||
@@ -412,7 +410,7 @@ export class Input extends Component<Props, State> {
|
||||
|
||||
{props.mode === 'main' && (
|
||||
<div className="input__rss">
|
||||
<div class="input__markdown">
|
||||
<div className="input__markdown">
|
||||
Styling with{' '}
|
||||
<a className="input__markdown-link" target="_blank" href="markdown-help.html">
|
||||
Markdown
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function BoldIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function CodeIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function HeaderIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function ImageIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function ItalicIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function LinkIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function OrderedListIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function QuoteIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
export default function UnorderedListIcon() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component } from 'preact';
|
||||
import '@github/markdown-toolbar-element';
|
||||
import BoldIcon from './markdown-toolbar-icons/bold-icon';
|
||||
import HeaderIcon from './markdown-toolbar-icons/header-icon';
|
||||
@@ -48,7 +48,7 @@ export default class MarkdownToolbar extends Component<Props> {
|
||||
await this.props.uploadImages(files);
|
||||
currentTarget.value = null;
|
||||
}
|
||||
render(props: RenderableProps<Props>) {
|
||||
render(props: Props) {
|
||||
return (
|
||||
<markdown-toolbar className="input__toolbar" for={props.textareaId}>
|
||||
<div className="input__toolbar-group">
|
||||
@@ -74,7 +74,7 @@ export default class MarkdownToolbar extends Component<Props> {
|
||||
</md-link>
|
||||
{this.props.allowUpload ? (
|
||||
<label className="input__toolbar-item" title={attachImageLabel} aria-label={attachImageLabel}>
|
||||
<input multiple class="input__toolbar-file-input" type="file" onChange={this.uploadImages} />
|
||||
<input multiple className="input__toolbar-file-input" type="file" onChange={this.uploadImages} />
|
||||
<ImageIcon />
|
||||
</label>
|
||||
) : null}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, Component } from 'preact';
|
||||
|
||||
type Props = JSX.HTMLAttributes & {
|
||||
autofocus: boolean;
|
||||
@@ -65,7 +65,7 @@ export default class TextareaAutosize extends Component<Props> {
|
||||
this.textareaRef.style.height = `${this.textareaRef.scrollHeight}px`;
|
||||
}
|
||||
}
|
||||
render(props: RenderableProps<Props>) {
|
||||
render(props: Props) {
|
||||
return (
|
||||
// We set text as a child of textarea and not in value property for a reason.
|
||||
// It's a workaround for the bug described here https://github.com/developit/preact/issues/326
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
import { NODE_ID } from '@app/common/constants';
|
||||
import { Comment as CommentType } from '@app/common/types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
import Preloader from './preloader';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX } from 'preact';
|
||||
import b, { Mix } from 'bem-react-helper';
|
||||
|
||||
type Props = JSX.HTMLAttributes & {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component } from 'preact';
|
||||
import { Component, JSX } from 'preact';
|
||||
import { sleep } from '@app/utils/sleep';
|
||||
|
||||
interface Props {
|
||||
children: (props: { inView: boolean; ref: (ref: Component) => Component }) => JSX.Element;
|
||||
children: (props: { inView: boolean; ref: (ref: Component) => unknown }) => JSX.Element;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -52,7 +52,7 @@ export class InView extends Component<Props, State> {
|
||||
const el = ref ? ref.base : undefined;
|
||||
if (el === this.state.ref) return;
|
||||
this.setState({
|
||||
ref: ref ? ref.base : undefined,
|
||||
ref: ref ? (ref.base as Element) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -65,8 +65,7 @@ export class InView extends Component<Props, State> {
|
||||
|
||||
render() {
|
||||
const props = { inView: this.state.inView, ref: this.refSetter };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const r = (this.props.children as any)[0](props);
|
||||
const r = this.props.children(props);
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component, FunctionComponent } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, Sorting, AuthProvider } from '@app/common/types';
|
||||
@@ -42,6 +42,7 @@ import { uploadImage, getPreview } from '@app/common/api';
|
||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
import postMessage from '@app/utils/postMessage';
|
||||
import { useActions } from '@app/hooks/useAction';
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
user: state.user,
|
||||
@@ -201,7 +202,7 @@ export class Root extends Component<Props, State> {
|
||||
return isUserAnonymous(this.props.user);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, { isLoaded, isCommentsListLoading, commentsShown }: State) {
|
||||
render(props: Props, { isLoaded, isCommentsListLoading, commentsShown }: State) {
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div id={NODE_ID}>
|
||||
@@ -325,7 +326,8 @@ export class Root extends Component<Props, State> {
|
||||
}
|
||||
|
||||
/** Root component connected to redux */
|
||||
export const ConnectedRoot = connect(
|
||||
mapStateToProps,
|
||||
boundActions
|
||||
)(Root);
|
||||
export const ConnectedRoot: FunctionComponent = () => {
|
||||
const props = useSelector(mapStateToProps);
|
||||
const actions = useActions(boundActions);
|
||||
return <Root {...props} {...actions} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, Component } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, BlockedUser, Theme, BlockTTL } from '@app/common/types';
|
||||
@@ -29,7 +29,7 @@ interface State {
|
||||
unhiddenUsers: (User['id'])[];
|
||||
}
|
||||
|
||||
export default class BlockedUsers extends Component<Props, State> {
|
||||
export default class Settings extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class BlockedUsers extends Component<Props, State> {
|
||||
return false;
|
||||
};
|
||||
|
||||
render({ user, theme }: RenderableProps<Props>, { blockedUsers, unblockedUsers, unhiddenUsers }: State) {
|
||||
render({ user, theme }: Props, { blockedUsers, unblockedUsers, unhiddenUsers }: State) {
|
||||
const hiddenUsersList = Object.values(this.state.hiddenUsers);
|
||||
return (
|
||||
<div className={b('settings', {}, { theme })}>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @jsx h */
|
||||
import { h, RenderableProps } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
/** @jsx createElement */
|
||||
import { createElement, RenderableProps, FunctionComponent } from 'preact';
|
||||
import { useStore } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { ConnectedComment as Comment } from '@app/components/comment/connected-comment';
|
||||
@@ -14,20 +14,22 @@ const mapStateToProps = (state: StoreState, props: { id: CommentInterface['id']
|
||||
return {
|
||||
comment,
|
||||
childs: state.childComments[props.id],
|
||||
collapsed: getThreadIsCollapsed(state, comment),
|
||||
collapsed: getThreadIsCollapsed(comment)(state),
|
||||
isCommentsDisabled: !!state.info.read_only,
|
||||
theme: state.theme,
|
||||
};
|
||||
};
|
||||
|
||||
type Props = {
|
||||
interface OwnProps {
|
||||
id: CommentInterface['id'];
|
||||
childs?: (CommentInterface['id'])[];
|
||||
level: number;
|
||||
mix?: string;
|
||||
|
||||
getPreview(text: string): Promise<string>;
|
||||
} & ReturnType<typeof mapStateToProps>;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ReturnType<typeof mapStateToProps>;
|
||||
|
||||
function Thread(props: RenderableProps<Props>) {
|
||||
const { collapsed, comment, childs, level, theme } = props;
|
||||
@@ -67,4 +69,7 @@ function Thread(props: RenderableProps<Props>) {
|
||||
);
|
||||
}
|
||||
|
||||
export const ConnectedThread = connect(mapStateToProps)(Thread);
|
||||
export const ConnectedThread: FunctionComponent<OwnProps> = props => {
|
||||
const providedProps = mapStateToProps(useStore().getState(), props);
|
||||
return <Thread {...props} {...providedProps} />;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement } from 'preact';
|
||||
|
||||
import { Comment as CommentType } from '@app/common/types';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
/** @jsx createElement */
|
||||
import { createElement, JSX, Component, FunctionComponent } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { connect } from 'preact-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { StoreState, StoreDispatch } from '@app/store';
|
||||
import { StoreState } from '@app/store';
|
||||
import { Comment } from '@app/common/types';
|
||||
import { fetchInfo } from '@app/store/user-info/actions';
|
||||
import { userInfo } from '@app/common/user-info-settings';
|
||||
@@ -11,11 +11,14 @@ import { userInfo } from '@app/common/user-info-settings';
|
||||
import LastCommentsList from './last-comments-list';
|
||||
import { AvatarIcon } from '../avatar-icon';
|
||||
import postMessage from '@app/utils/postMessage';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
import { useActions } from '@app/hooks/useAction';
|
||||
|
||||
interface Props {
|
||||
const boundActions = bindActions({ fetchInfo });
|
||||
|
||||
type Props = {
|
||||
comments: Comment[] | null;
|
||||
fetchInfo: () => Promise<Comment[] | null>;
|
||||
}
|
||||
} & typeof boundActions;
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
@@ -44,9 +47,10 @@ class UserInfo extends Component<Props, State> {
|
||||
document.removeEventListener('keydown', UserInfo.onKeyDown);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, state: State): JSX.Element | null {
|
||||
render(): JSX.Element | null {
|
||||
const user = userInfo;
|
||||
const { comments = [] } = props;
|
||||
const { comments = [] } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
// TODO: handle
|
||||
if (!user) {
|
||||
@@ -59,7 +63,7 @@ class UserInfo extends Component<Props, State> {
|
||||
<p className="user-info__title">Last comments by {user.name}</p>
|
||||
<p className="user-info__id">{user.id}</p>
|
||||
|
||||
{!!comments && <LastCommentsList isLoading={state.isLoading} comments={comments} />}
|
||||
{!!comments && <LastCommentsList isLoading={isLoading} comments={comments} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -76,17 +80,10 @@ class UserInfo extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: StoreDispatch) => ({
|
||||
fetchInfo: () => dispatch(fetchInfo()),
|
||||
});
|
||||
const commentsSelector = (state: StoreState) => state.userComments![userInfo.id!];
|
||||
|
||||
export const ConnectedUserInfo = connect(
|
||||
(
|
||||
state: StoreState
|
||||
): {
|
||||
comments: Comment[] | null;
|
||||
} => ({
|
||||
comments: state.userComments![userInfo.id!],
|
||||
}),
|
||||
mapDispatchToProps
|
||||
)(UserInfo);
|
||||
export const ConnectedUserInfo: FunctionComponent = () => {
|
||||
const comments = useSelector(commentsSelector);
|
||||
const actions = useActions(boundActions);
|
||||
return <UserInfo comments={comments} {...actions} />;
|
||||
};
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
/** @jsx h */
|
||||
import { AnyComponent } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
import { StoreState } from '@app/store';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
/**
|
||||
* Connects redux theme property to component's
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
function withTheme<P extends { theme: Theme }, S extends object>(PlainComponent: AnyComponent<P, S>) {
|
||||
return connect((state: StoreState) => ({ theme: state.theme }))(PlainComponent);
|
||||
}
|
||||
|
||||
export default withTheme;
|
||||
@@ -0,0 +1,23 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, FunctionComponent, ComponentType } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StoreState } from '@app/store';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
const themeSelector = (state: StoreState) => state.theme;
|
||||
|
||||
/**
|
||||
* Connects redux theme property to component's
|
||||
*/
|
||||
function withTheme<P extends { theme: Theme }>(PlainComponent: ComponentType<P>) {
|
||||
const o: FunctionComponent<Omit<P, 'theme'>> = props => {
|
||||
const theme = useSelector(themeSelector);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return <PlainComponent theme={theme} {...(props as any)} />;
|
||||
};
|
||||
o.displayName = `withTheme(${PlainComponent.displayName || PlainComponent.name})`;
|
||||
|
||||
return o;
|
||||
}
|
||||
|
||||
export default withTheme;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { useCallback, useMemo } from 'preact/compat';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { BoundActionCreator, BoundActionCreators } from '@app/utils/actionBinder';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/** binds actions to dispatch */
|
||||
export const useActions = <Actions extends { [key: string]: Function }>(
|
||||
actions: Actions
|
||||
): BoundActionCreators<Actions> => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.entries(actions).reduce<BoundActionCreator<Actions>>(
|
||||
(result, [key, fn]) => {
|
||||
(result as any)[key] = (...args: any[]) => dispatch(fn(...args));
|
||||
return result;
|
||||
},
|
||||
{} as any
|
||||
),
|
||||
[dispatch, ...Object.values(actions)]
|
||||
) as any;
|
||||
};
|
||||
|
||||
export const useAction = <Action extends Function>(action: Action): BoundActionCreator<Action> => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
return useCallback(((...args: any[]) => dispatch(action(...args))) as any, [dispatch, action]);
|
||||
};
|
||||
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable no-console, @typescript-eslint/camelcase */
|
||||
/** @jsx h */
|
||||
/** @jsx createElement */
|
||||
declare let remark_config: LastCommentsConfig;
|
||||
|
||||
import loadPolyfills from '@app/common/polyfills';
|
||||
import '@app/utils/patchPreactContext';
|
||||
import { h, render } from 'preact';
|
||||
import { createElement, render } from 'preact';
|
||||
import 'preact/debug';
|
||||
import { getLastComments } from './common/api';
|
||||
import { LastCommentsConfig } from '@app/common/config-types';
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
/* eslint-disable no-console */
|
||||
/** @jsx h */
|
||||
/** @jsx createElement */
|
||||
import loadPolyfills from '@app/common/polyfills';
|
||||
import '@app/utils/patchPreactContext';
|
||||
import { h, render } from 'preact';
|
||||
import { createElement, render } from 'preact';
|
||||
import 'preact/debug';
|
||||
|
||||
import { Provider } from 'preact-redux';
|
||||
import { Provider } from 'react-redux';
|
||||
import { ConnectedRoot } from '@app/components/root';
|
||||
import { UserInfo } from '@app/components/user-info';
|
||||
import reduxStore from '@app/store';
|
||||
@@ -69,7 +68,6 @@ async function init(): Promise<void> {
|
||||
</Provider>
|
||||
</div>
|
||||
</div>,
|
||||
node.parentElement!,
|
||||
node
|
||||
);
|
||||
} else {
|
||||
@@ -77,7 +75,6 @@ async function init(): Promise<void> {
|
||||
<Provider store={reduxStore}>
|
||||
<ConnectedRoot />
|
||||
</Provider>,
|
||||
node.parentElement!,
|
||||
node
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Comment, CommentMode } from '@app/common/types';
|
||||
import { StoreState } from '../index';
|
||||
|
||||
export const getCommentMode = (state: StoreState, id: Comment['id']): CommentMode => {
|
||||
export const getCommentMode = (id: Comment['id']) => (state: StoreState): CommentMode => {
|
||||
if (state.activeComment === null) {
|
||||
return CommentMode.None;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { StaticStore } from '@app/common/static_store';
|
||||
|
||||
import { StoreState } from '../index';
|
||||
|
||||
export const getThreadIsCollapsed = (state: StoreState, comment: Comment): boolean => {
|
||||
export const getThreadIsCollapsed = (comment: Comment) => (state: StoreState): boolean => {
|
||||
const collapsed = state.collapsedThreads[comment.id];
|
||||
|
||||
if (collapsed !== null && collapsed !== undefined) {
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// hack for https://github.com/developit/preact-compat/issues/475
|
||||
// should be changed when preact@10 is available
|
||||
|
||||
import React from 'preact';
|
||||
import { createContext } from 'preact-context';
|
||||
|
||||
React.createContext = createContext;
|
||||
@@ -10,6 +10,8 @@ module.exports = {
|
||||
moduleNameMapper: {
|
||||
'\\.scss$': '<rootDir>/app/testUtils/mockStyles.js',
|
||||
'@app/(.*)': '<rootDir>/app/$1',
|
||||
'^react$': 'preact/compat',
|
||||
'^react-dom$': 'preact/compat',
|
||||
},
|
||||
setupFilesAfterEnv: ['<rootDir>/app/testUtils/index.ts'],
|
||||
};
|
||||
|
||||
Generated
+84
-14
@@ -1793,6 +1793,15 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/hoist-non-react-statics": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz",
|
||||
"integrity": "sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA==",
|
||||
"requires": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz",
|
||||
@@ -1863,6 +1872,31 @@
|
||||
"integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw=="
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.5.tgz",
|
||||
"integrity": "sha512-jQ12VMiFOWYlp+j66dghOWcmDDwhca0bnlcTxS4Qz/fh5gi6wpaZDthPEu/Gc/YlAuO87vbiUXL8qKstFvuOaA==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"@types/react-redux": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.4.tgz",
|
||||
"integrity": "sha512-SUV/7d+4L7C1Db/D4pqASgN1V1U2HnDEhEol9lYpPSguS76xFboZzf5ha2hTz6v31cUewyC7WksMh1q8JxhebQ==",
|
||||
"requires": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/redux-mock-store": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/redux-mock-store/-/redux-mock-store-1.0.1.tgz",
|
||||
@@ -4264,6 +4298,11 @@
|
||||
"cssom": "0.3.x"
|
||||
}
|
||||
},
|
||||
"csstype": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.7.tgz",
|
||||
"integrity": "sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ=="
|
||||
},
|
||||
"cuint": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
|
||||
@@ -6866,6 +6905,14 @@
|
||||
"minimalistic-crypto-utils": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"hoist-non-react-statics": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
|
||||
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
|
||||
"requires": {
|
||||
"react-is": "^16.7.0"
|
||||
}
|
||||
},
|
||||
"homedir-polyfill": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
|
||||
@@ -7398,7 +7445,6 @@
|
||||
"version": "2.2.4",
|
||||
"resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
|
||||
"integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
@@ -10799,8 +10845,7 @@
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
|
||||
"dev": true
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"object-copy": {
|
||||
"version": "0.1.0",
|
||||
@@ -11829,9 +11874,9 @@
|
||||
}
|
||||
},
|
||||
"preact": {
|
||||
"version": "8.5.2",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-8.5.2.tgz",
|
||||
"integrity": "sha512-37tlDJGq5IQKqGUbqPZ7qPtsTOWFyxe+ojAOFfzKo0dEPreenqrqgJuS83zGpeGAqD9h9L9Yr7QuxH2W4ZrKxg=="
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.0.1.tgz",
|
||||
"integrity": "sha512-lq7jo1rwwCd1YkiBcuOxRc3I0y1FZACa6O7tgNXt47QZJtSlLEE53f/FDNsLtiB2IVQTHbaey20TjSPmejhDyQ=="
|
||||
},
|
||||
"preact-context": {
|
||||
"version": "1.1.4",
|
||||
@@ -11840,9 +11885,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"preact-redux": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/preact-redux/-/preact-redux-2.0.3.tgz",
|
||||
"integrity": "sha1-lgpTXDImQ801mY8z8MLme8Hn6qs="
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/preact-redux/-/preact-redux-2.1.0.tgz",
|
||||
"integrity": "sha512-IFjT1f5I7siBtb/jsqQbOY6QUoYHHfiHhVaxvT+C9Q++h2C7YULSC30B4gVru3Phgwgs+qWb+GUiS5EKIQb19g=="
|
||||
},
|
||||
"preact-render-to-string": {
|
||||
"version": "4.1.0",
|
||||
@@ -11972,7 +12017,6 @@
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -12153,8 +12197,35 @@
|
||||
"react-is": {
|
||||
"version": "16.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
|
||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
|
||||
},
|
||||
"react-redux": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.1.1.tgz",
|
||||
"integrity": "sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"invariant": "^2.2.4",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^16.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.2.tgz",
|
||||
"integrity": "sha512-EXxN64agfUqqIGeEjI5dL5z0Sw0ZwWo1mLTi4mQowCZ42O59b7DRpZAnTC6OqdF28wMBMFKNb/4uFGrVaigSpg==",
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "16.10.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz",
|
||||
"integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "3.0.0",
|
||||
@@ -12253,8 +12324,7 @@
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
|
||||
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
|
||||
},
|
||||
"regenerator-transform": {
|
||||
"version": "0.14.1",
|
||||
|
||||
@@ -94,13 +94,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@github/markdown-toolbar-element": "^1.1.0",
|
||||
"@types/react-redux": "^7.1.4",
|
||||
"@webcomponents/custom-elements": "^1.3.0",
|
||||
"bem-react-helper": "^1.1.2",
|
||||
"core-js": "^3.2.1",
|
||||
"focus-visible": "^5.0.2",
|
||||
"intersection-observer": "^0.7.0",
|
||||
"preact": "^8.5.2",
|
||||
"preact-redux": "^2.0.3",
|
||||
"preact": "^10.0.1",
|
||||
"preact-redux": "^2.1.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"redux": "^4.0.4",
|
||||
"redux-thunk": "^2.3.0"
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"*": ["@types/*"],
|
||||
"@app/*": ["app/*"]
|
||||
"@app/*": ["app/*"],
|
||||
}
|
||||
},
|
||||
"include": ["app/**/*"],
|
||||
|
||||
@@ -76,7 +76,11 @@ module.exports = () => ({
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js'],
|
||||
alias: { '@app': path.resolve(__dirname, 'app') },
|
||||
alias: {
|
||||
'@app': path.resolve(__dirname, 'app'),
|
||||
react: 'preact/compat',
|
||||
'react-dom': 'preact/compat',
|
||||
},
|
||||
modules: [path.resolve(__dirname, 'node_modules')],
|
||||
},
|
||||
module: {
|
||||
|
||||
Reference in New Issue
Block a user