profile popup
This commit is contained in:
@@ -12,6 +12,11 @@ module.exports = {
|
||||
ignore: ['after-comment'],
|
||||
},
|
||||
],
|
||||
'comment-empty-line-before': [
|
||||
'always',
|
||||
{ except: ['first-nested'], ignore: ['after-comment', 'stylelint-commands'] },
|
||||
],
|
||||
'value-keyword-case': ['lower', { ignoreProperties: ['composes'] }],
|
||||
'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global'] }],
|
||||
'property-no-unknown': [true, { ignoreProperties: ['composes'] }],
|
||||
'mavrin/stylelint-declaration-use-css-custom-properties': {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
jest.mock('react-intl', () => {
|
||||
const messages = require('locales/en.json');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
@@ -11,13 +11,8 @@ export const getPostComments = (sort: Sorting) => apiFetcher.get<Tree>('/find',
|
||||
|
||||
export const getComment = (id: Comment['id']): Promise<Comment> => apiFetcher.get(`/id/${id}`, { url });
|
||||
|
||||
export const getUserComments = (
|
||||
userId: User['id'],
|
||||
limit: number
|
||||
): Promise<{
|
||||
comments: Comment[];
|
||||
count: number;
|
||||
}> => apiFetcher.get('/comments', { user: userId, limit });
|
||||
export const getUserComments = (userId: User['id']): Promise<{ comments: Comment[] }> =>
|
||||
apiFetcher.get('/comments', { user: userId, limit: 10 });
|
||||
|
||||
export const putCommentVote = ({ id, value }: { id: Comment['id']; value: number }): Promise<void> =>
|
||||
apiFetcher.put(`/vote/${id}`, { url, vote: value });
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseQuery } from 'utils/parseQuery';
|
||||
import { parseQuery } from 'utils/parse-query';
|
||||
|
||||
import type { Theme } from './types';
|
||||
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
|
||||
|
||||
@@ -10,7 +10,7 @@ export type User = {
|
||||
};
|
||||
|
||||
/** data which is used on user-info page */
|
||||
export type UserInfo = Pick<User, 'id' | 'name' | 'picture'>;
|
||||
export type Profile = Pick<User, 'id' | 'name' | 'picture'> & { current?: '1' };
|
||||
|
||||
export interface BlockedUser {
|
||||
id: string;
|
||||
@@ -46,7 +46,7 @@ export interface Comment {
|
||||
* if user hasn't voted delta will be 0,
|
||||
* -1/+1 for downvote/upvote
|
||||
*/
|
||||
vote: number;
|
||||
vote: 0 | 1 | -1;
|
||||
/** comment controversy, read only */
|
||||
controversy?: number;
|
||||
/** pointer to have empty default in json response */
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.auth-panel__user-id {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-panel__user-dropdown-title {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.auth-panel_logged-in {
|
||||
font-size: 12px;
|
||||
|
||||
& .auth-panel__column {
|
||||
&:first-child {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.auth-panel_theme_dark {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.auth-panel_theme_light {
|
||||
& .auth-panel__user-id {
|
||||
color: var(--color13);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.userButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
color: rgb(var(--secondary-text-color));
|
||||
transition: color 0.15s;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.userLogoutButton {
|
||||
composes: userButton;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.userAvatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@@ -80,7 +80,7 @@ describe('<AuthPanel />', () => {
|
||||
|
||||
const userInfo = authPanelColumn.first();
|
||||
|
||||
expect(userInfo.text()).toEqual(expect.stringContaining('You logged in as John'));
|
||||
expect(userInfo.text()).toEqual(expect.stringContaining('John'));
|
||||
});
|
||||
});
|
||||
describe('For admin user', () => {
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { h, Component } from 'preact';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FormattedMessage, defineMessages, IntlShape, useIntl } from 'react-intl';
|
||||
import b from 'bem-react-helper';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { User, Sorting, Theme, PostInfo } from 'common/types';
|
||||
import { IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from 'common/constants';
|
||||
import { requestDeletion } from 'utils/email';
|
||||
import { postMessageToParent } from 'utils/postMessage';
|
||||
import { postMessageToParent } from 'utils/post-message';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { StoreState } from 'store';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
import { useTheme } from 'hooks/useTheme';
|
||||
import { Button } from 'components/button';
|
||||
import { Auth } from 'components/auth';
|
||||
import { useTheme } from 'hooks/useTheme';
|
||||
import { Avatar } from 'components/avatar';
|
||||
import { SignOutIcon } from 'components/icons/signout';
|
||||
import { IconButton } from 'components/icon-button/icon-button';
|
||||
import { messages } from 'components/auth/auth.messsages';
|
||||
|
||||
import styles from './auth-panel.module.css';
|
||||
|
||||
interface OwnProps {
|
||||
user: User | null;
|
||||
@@ -20,8 +25,8 @@ interface OwnProps {
|
||||
isCommentsDisabled: boolean;
|
||||
postInfo: PostInfo;
|
||||
|
||||
signout(): Promise<void>;
|
||||
onSortChange(s: Sorting): Promise<void>;
|
||||
onSignOut(): Promise<void>;
|
||||
onCommentsChangeReadOnlyMode(readOnly: boolean): Promise<void>;
|
||||
onBlockedUsersShow(): void;
|
||||
onBlockedUsersHide(): void;
|
||||
@@ -72,46 +77,25 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
this.props.onCommentsChangeReadOnlyMode(!this.props.isCommentsDisabled);
|
||||
};
|
||||
|
||||
toggleUserInfoVisibility = () => {
|
||||
const { user } = this.props;
|
||||
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
|
||||
postMessageToParent({ profile: user });
|
||||
};
|
||||
|
||||
renderAuthorized = (user: User) => {
|
||||
const { onSignOut, theme } = this.props;
|
||||
const isUserAnonymous = user && user.id.substr(0, 10) === 'anonymous_';
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage id="authPanel.logged-as" defaultMessage="You logged in as" />{' '}
|
||||
<Dropdown title={user.name} titleClass="auth-panel__user-dropdown-title" theme={theme}>
|
||||
<DropdownItem separator={!isUserAnonymous}>
|
||||
<div
|
||||
id={user.id}
|
||||
className={b('auth-panel__user-id', {}, { theme })}
|
||||
{...getHandleClickProps(this.toggleUserInfoVisibility)}
|
||||
>
|
||||
{user.id}
|
||||
</div>
|
||||
</DropdownItem>
|
||||
|
||||
{!isUserAnonymous && (
|
||||
<DropdownItem>
|
||||
<Button theme={theme} onClick={() => requestDeletion().then(onSignOut)}>
|
||||
<FormattedMessage id="authPanel.request-to-delete-data" defaultMessage="Request my data removal" />
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>{' '}
|
||||
<Button kind="link" theme={theme} onClick={onSignOut}>
|
||||
<FormattedMessage id="authPanel.logout" defaultMessage="Logout?" />
|
||||
</Button>
|
||||
</>
|
||||
<div className={clsx('user', styles.user)}>
|
||||
<button
|
||||
className={clsx('user-profile-button', styles.userButton)}
|
||||
onClick={() => postMessageToParent({ profile: { ...user, current: '1' } })}
|
||||
title={this.props.intl.formatMessage(messages.openProfile)}
|
||||
>
|
||||
<div className={clsx('user-avatar', styles.userAvatar)}>
|
||||
<Avatar url={user.picture} />
|
||||
</div>
|
||||
{user.name}
|
||||
</button>{' '}
|
||||
<div className={clsx('user-logout-button', styles.userLogoutButton)}>
|
||||
<IconButton title={this.props.intl.formatMessage(messages.signout)} onClick={this.props.signout}>
|
||||
<SignOutIcon size="14" />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,7 +120,9 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
};
|
||||
|
||||
renderCookiesWarning = () => {
|
||||
if (IS_STORAGE_AVAILABLE || IS_THIRD_PARTY) return null;
|
||||
if (IS_STORAGE_AVAILABLE || IS_THIRD_PARTY) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="auth-panel__column">
|
||||
<FormattedMessage id="authPanel.enable-cookies" defaultMessage="Allow cookies to login and comment" />
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import './auth-panel.css';
|
||||
|
||||
import './__readonly-label/auth-panel__readonly-label.css';
|
||||
|
||||
import './__column/auth-panel__column.css';
|
||||
import './__select/auth-panel__select.css';
|
||||
import './__select-label/auth-panel__select-label.css';
|
||||
import './__select-label-value/auth-panel__select-label-value.css';
|
||||
import './__sort/auth-panel__sort.css';
|
||||
|
||||
import './__user-id/auth-panel__user-id.css';
|
||||
|
||||
import './_theme/_dark/auth-panel_theme_dark.css';
|
||||
import './_theme/_light/auth-panel_theme_light.css';
|
||||
|
||||
import './_logged-in/auth-panel_logged-in.css';
|
||||
|
||||
export * from './auth-panel';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { parseMessage, postMessageToParent } from 'utils/postMessage';
|
||||
import { parseMessage, postMessageToParent } from 'utils/post-message';
|
||||
|
||||
function handleChangeIframeSize(element: HTMLElement) {
|
||||
const { top } = element.getBoundingClientRect();
|
||||
|
||||
@@ -57,4 +57,12 @@ export const messages = defineMessages({
|
||||
id: 'auth.submit',
|
||||
defaultMessage: 'Submit',
|
||||
},
|
||||
openProfile: {
|
||||
id: 'auth.open-profile',
|
||||
defaultMessage: 'Open My Profile',
|
||||
},
|
||||
signout: {
|
||||
id: 'auth.signout',
|
||||
defaultMessage: 'Sign Out',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -25,15 +25,11 @@
|
||||
left: 0;
|
||||
min-width: 240px;
|
||||
padding: 16px;
|
||||
background-color: rgb(var(--white-color));
|
||||
background-color: rgb(var(--primary-background-color));
|
||||
box-shadow: 0 10px 15px rgba(var(--black-color), 0.1), 0 -1px 6px rgba(var(--black-color), 0.05);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:global(.dark) .dropdown {
|
||||
background-color: var(--color8);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 12px;
|
||||
@@ -148,16 +144,14 @@
|
||||
|
||||
.closeButton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
background: unset;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
color: inherit;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--primary-color), 0.1);
|
||||
@@ -194,15 +188,6 @@
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(var(--white-color), 0.2);
|
||||
border-right-color: rgb(var(--white-color));
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-bottom: 12px;
|
||||
padding: 6px 8px;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { h } from 'preact';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/preact';
|
||||
import { fireEvent, waitFor } from '@testing-library/preact';
|
||||
import { render } from 'tests/utils';
|
||||
|
||||
import { OAuthProvider, User } from 'common/types';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useIntl } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { setUser } from 'store/user/actions';
|
||||
import { Input } from 'components/input';
|
||||
import { CrossIcon } from 'components/icons/cross';
|
||||
import { TextareaAutosize } from 'components/textarea-autosize';
|
||||
|
||||
import { Button } from './components/button';
|
||||
@@ -16,6 +17,7 @@ import { getProviders, getTokenInvalidReason } from './auth.utils';
|
||||
import { emailSignin, verifyEmailSignin, anonymousSignin } from './auth.api';
|
||||
|
||||
import styles from './auth.module.css';
|
||||
import { Spinner } from 'components/spinner/spinner';
|
||||
|
||||
export function Auth() {
|
||||
const intl = useIntl();
|
||||
@@ -107,15 +109,7 @@ export function Auth() {
|
||||
<>
|
||||
{errorMessage && <div className={clsx('auth-error', styles.error)}>{errorMessage}</div>}
|
||||
<Button className="auth-submit" type="submit" disabled={isLoading}>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={clsx('spinner', styles.spinner)}
|
||||
role="presentation"
|
||||
aria-label={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
) : (
|
||||
intl.formatMessage(messages.submit)
|
||||
)}
|
||||
{isLoading ? <Spinner /> : intl.formatMessage(messages.submit)}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
@@ -146,7 +140,7 @@ export function Auth() {
|
||||
<>
|
||||
<div className={clsx('auth-row', styles.row)}>
|
||||
<div className={styles.backButton}>
|
||||
<Button className="auth-back-button" size="small" kind="transparent" onClick={handleShowEmailStep}>
|
||||
<Button className="auth-back-button" size="xs" kind="transparent" onClick={handleShowEmailStep}>
|
||||
<svg
|
||||
className={styles.backButtonArrow}
|
||||
width="14"
|
||||
@@ -171,15 +165,7 @@ export function Auth() {
|
||||
title="Close sign-in dropdown"
|
||||
onClick={handleDropdownClose}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
<CrossIcon />
|
||||
</button>
|
||||
</div>
|
||||
<div className={clsx('auth-row', styles.row)}>
|
||||
|
||||
@@ -56,12 +56,16 @@
|
||||
background-color: rgb(var(--primary-color));
|
||||
}
|
||||
|
||||
.small {
|
||||
.xs {
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sm {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.transparent {
|
||||
background-color: rgba(var(--primary-color), 0.1);
|
||||
color: rgb(var(--primary-color));
|
||||
@@ -72,6 +76,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
color: rgb(var(--primary-color));
|
||||
background-color: unset;
|
||||
text-transform: initial;
|
||||
width: auto;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--primary-color), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dark) {
|
||||
& .button {
|
||||
border-color: rgba(var(--white-color), 0.1);
|
||||
|
||||
@@ -4,8 +4,8 @@ import clsx from 'clsx';
|
||||
import styles from './button.module.css';
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'size'> & {
|
||||
size?: 'small';
|
||||
kind?: 'transparent';
|
||||
size?: 'xs' | 'sm';
|
||||
kind?: 'transparent' | 'link';
|
||||
suffix?: VNode;
|
||||
loading?: boolean;
|
||||
selected?: boolean;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/preact';
|
||||
import { fireEvent, waitFor } from '@testing-library/preact';
|
||||
import { render } from 'tests/utils';
|
||||
|
||||
import type { User } from 'common/types';
|
||||
import * as userActions from 'store/user/actions';
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
.avatar {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,19 +2,18 @@ import '@testing-library/jest-dom';
|
||||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
|
||||
import { BASE_URL } from 'common/constants.config';
|
||||
|
||||
import { Avatar } from './avatar';
|
||||
import { BASE_URL } from 'common/constants.config';
|
||||
|
||||
describe('<Avatar/>', () => {
|
||||
it('should have correct url', () => {
|
||||
const { container } = render(<Avatar className="avatar" />);
|
||||
const { container } = render(<Avatar />);
|
||||
|
||||
expect(container.querySelector('img')).toHaveAttribute('src', `${BASE_URL}/image.svg`);
|
||||
});
|
||||
|
||||
it("shouldn't be accessible with screen reader", () => {
|
||||
const { container } = render(<Avatar className="avatar" />);
|
||||
const { container } = render(<Avatar />);
|
||||
|
||||
expect(container.querySelector('img')).toHaveAttribute('aria-hidden', 'true');
|
||||
});
|
||||
|
||||
@@ -8,19 +8,13 @@ import styles from './avatar.module.css';
|
||||
|
||||
type Props = {
|
||||
url?: string;
|
||||
/** className should be used only in puprose of put permanent class on Avatar for user themization */
|
||||
className: string;
|
||||
};
|
||||
|
||||
export function Avatar({ url, className }: Props) {
|
||||
export function Avatar({ url }: Props) {
|
||||
const avatarUrl = url || `${BASE_URL}${ghostIconUrl}`;
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<img
|
||||
className={clsx('avatar', className, styles.avatar, !url && styles.avatarGhost)}
|
||||
src={avatarUrl}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<img className={clsx('avatar', styles.avatar, !url && styles.avatarGhost)} src={avatarUrl} aria-hidden="true" />
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -294,7 +294,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
type="submit"
|
||||
disabled={!isValidEmailAddress || loading}
|
||||
>
|
||||
{loading ? <Preloader mix="comment-form__subscribe-by-email__preloader" /> : buttonLabel}
|
||||
{loading ? <Preloader className="comment-form__subscribe-by-email__preloader" /> : buttonLabel}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
+11
@@ -7,6 +7,17 @@ jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn((fn) => fn({ theme: 'light' })),
|
||||
}));
|
||||
|
||||
jest.mock('react-intl', () => {
|
||||
const messages = require('locales/en.json');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
|
||||
describe('<SubscribeByRSS/>', () => {
|
||||
it('should be render links in dropdown', () => {
|
||||
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
|
||||
+2
-4
@@ -1,6 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#4fbbd6" d="M5.823 14.822l-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z"/>
|
||||
<path stroke="#FFF" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M4.755 8.252L7 10.5l4.495-4.495"/>
|
||||
</g>
|
||||
<path fill="#4fbbd6" d="M5.823 14.822l-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z" />
|
||||
<path stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" d="M4.755 8.252L7 10.5l4.495-4.495" />
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.0 KiB |
@@ -14,6 +14,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.comment_level_6 {
|
||||
& .comment__text,
|
||||
& .comment__actions {
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
import { h } from 'preact';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
jest.mock('react-intl', () => {
|
||||
const messages = require('locales/en.json');
|
||||
const reactIntl = jest.requireActual('react-intl');
|
||||
const intlProvider = new reactIntl.IntlProvider({ locale: 'en', messages }, {});
|
||||
|
||||
return {
|
||||
...reactIntl,
|
||||
useIntl: () => intlProvider.state.intl,
|
||||
};
|
||||
});
|
||||
|
||||
import { useIntl, IntlProvider } from 'react-intl';
|
||||
|
||||
import enMessages from 'locales/en.json';
|
||||
@@ -24,7 +36,6 @@ function mountComment(props: CommentProps) {
|
||||
}
|
||||
|
||||
const DefaultProps: Partial<CommentProps> = {
|
||||
CommentForm: null,
|
||||
post_info: {
|
||||
read_only: false,
|
||||
} as PostInfo,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { h, JSX, Component, createRef, ComponentType } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { API_BASE, BASE_URL, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants';
|
||||
import { COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants';
|
||||
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { debounce } from 'utils/debounce';
|
||||
@@ -16,7 +16,7 @@ import { Avatar } from 'components/avatar';
|
||||
import { Button } from 'components/button';
|
||||
import { Countdown } from 'components/countdown';
|
||||
import { getPreview, uploadImage } from 'common/api';
|
||||
import { postMessageToParent } from 'utils/postMessage';
|
||||
import { postMessageToParent } from 'utils/post-message';
|
||||
import { FormattedMessage, IntlShape, defineMessages } from 'react-intl';
|
||||
import { getVoteMessage, VoteMessagesTypes } from './getVoteMessage';
|
||||
import { getBlockingDurations } from './getBlockingDurations';
|
||||
@@ -24,79 +24,12 @@ import { boundActions } from './connected-comment';
|
||||
|
||||
import './styles';
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: {
|
||||
id: 'comment.delete-message',
|
||||
defaultMessage: 'Do you want to delete this comment?',
|
||||
},
|
||||
hideUserComments: {
|
||||
id: 'comment.hide-user-comment',
|
||||
defaultMessage: 'Do you want to hide comments of {userName}?',
|
||||
},
|
||||
pinComment: {
|
||||
id: 'comment.pin-comment',
|
||||
defaultMessage: 'Do you want to pin this comment?',
|
||||
},
|
||||
unpinComment: {
|
||||
id: 'comment.unpin-comment',
|
||||
defaultMessage: 'Do you want to unpin this comment?',
|
||||
},
|
||||
verifyUser: {
|
||||
id: 'comment.verify-user',
|
||||
defaultMessage: 'Do you want to verify {userName}?',
|
||||
},
|
||||
unverifyUser: {
|
||||
id: 'comment.unverify-user',
|
||||
defaultMessage: 'Do you want to unverify {userName}?',
|
||||
},
|
||||
blockUser: {
|
||||
id: 'comment.block-user',
|
||||
defaultMessage: 'Do you want to block {userName} {duration}?',
|
||||
},
|
||||
unblockUser: {
|
||||
id: 'comment.unblock-user',
|
||||
defaultMessage: 'Do you want to unblock this user?',
|
||||
},
|
||||
deletedComment: {
|
||||
id: 'comment.deleted-comment',
|
||||
defaultMessage: 'This comment was deleted',
|
||||
},
|
||||
controversy: {
|
||||
id: 'comment.controversy',
|
||||
defaultMessage: 'Controversy: {value}',
|
||||
},
|
||||
toggleVerification: {
|
||||
id: 'comment.toggle-verification',
|
||||
defaultMessage: 'Toggle verification',
|
||||
},
|
||||
verifiedUser: {
|
||||
id: 'comment.verified-user',
|
||||
defaultMessage: 'Verified user',
|
||||
},
|
||||
unverifiedUser: {
|
||||
id: 'comment.unverified-user',
|
||||
defaultMessage: 'Unverified user',
|
||||
},
|
||||
goToParent: {
|
||||
id: 'comment.go-to-parent',
|
||||
defaultMessage: 'Go to parent comment',
|
||||
},
|
||||
expiredTime: {
|
||||
id: 'comment.expired-time',
|
||||
defaultMessage: 'Editing time has expired.',
|
||||
},
|
||||
commentTime: {
|
||||
id: 'comment.time',
|
||||
defaultMessage: '{day} at {time}',
|
||||
},
|
||||
});
|
||||
|
||||
export type CommentProps = {
|
||||
user: User | null;
|
||||
CommentForm: ComponentType<CommentFormProps> | null;
|
||||
CommentForm?: ComponentType<CommentFormProps>;
|
||||
data: CommentType;
|
||||
repliesCount?: number;
|
||||
post_info: PostInfo | null;
|
||||
post_info?: PostInfo;
|
||||
/** whether comment's user is banned */
|
||||
isUserBanned?: boolean;
|
||||
isCommentsDisabled: boolean;
|
||||
@@ -418,7 +351,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
const intl = this.props.intl;
|
||||
if (!(this.props.view === 'main' || this.props.view === 'pinned'))
|
||||
return getVoteMessage(VoteMessagesTypes.ONLY_POST_PAGE, intl);
|
||||
if (this.props.post_info!.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
|
||||
if (this.props.post_info?.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
|
||||
if (this.props.data.delete) return getVoteMessage(VoteMessagesTypes.DELETED, intl);
|
||||
if (this.isCurrentUser()) return getVoteMessage(VoteMessagesTypes.OWN_COMMENT, intl);
|
||||
if (StaticStore.config.positive_score && this.props.data.score < 1)
|
||||
@@ -435,7 +368,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
const intl = this.props.intl;
|
||||
if (!(this.props.view === 'main' || this.props.view === 'pinned'))
|
||||
return getVoteMessage(VoteMessagesTypes.ONLY_POST_PAGE, intl);
|
||||
if (this.props.post_info!.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
|
||||
if (this.props.post_info?.read_only) return getVoteMessage(VoteMessagesTypes.READONLY, intl);
|
||||
if (this.props.data.delete) return getVoteMessage(VoteMessagesTypes.DELETED, intl);
|
||||
if (this.isCurrentUser()) return getVoteMessage(VoteMessagesTypes.OWN_COMMENT, intl);
|
||||
if (this.isGuest()) return getVoteMessage(VoteMessagesTypes.GUEST, intl);
|
||||
@@ -463,14 +396,14 @@ export class Comment extends Component<CommentProps, State> {
|
||||
<FormattedMessage id="comment.copied" defaultMessage="Copied!" />
|
||||
</span>
|
||||
) : (
|
||||
<Button kind="link" {...getHandleClickProps(this.copyComment)} mix="comment__control">
|
||||
<Button kind="link" onClick={this.copyComment} mix="comment__control">
|
||||
<FormattedMessage id="comment.copy" defaultMessage="Copy" />
|
||||
</Button>
|
||||
)
|
||||
);
|
||||
|
||||
controls.push(
|
||||
<Button kind="link" {...getHandleClickProps(this.togglePin)} mix="comment__control">
|
||||
<Button kind="link" onClick={this.togglePin} mix="comment__control">
|
||||
{this.props.data.pin ? (
|
||||
<FormattedMessage id="comment.unpin" defaultMessage="Unpin" />
|
||||
) : (
|
||||
@@ -482,7 +415,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
|
||||
if (!isCurrentUser) {
|
||||
controls.push(
|
||||
<Button kind="link" {...getHandleClickProps(this.hideUser)} mix="comment__control">
|
||||
<Button kind="link" onClick={this.hideUser} mix="comment__control">
|
||||
<FormattedMessage id="comment.hide" defaultMessage="Hide" />
|
||||
</Button>
|
||||
);
|
||||
@@ -491,7 +424,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
if (isAdmin) {
|
||||
if (this.props.isUserBanned) {
|
||||
controls.push(
|
||||
<Button kind="link" {...getHandleClickProps(this.onUnblockUserClick)} mix="comment__control">
|
||||
<Button kind="link" onClick={this.onUnblockUserClick} mix="comment__control">
|
||||
<FormattedMessage id="comment.unblock" defaultMessage="Unblock" />
|
||||
</Button>
|
||||
);
|
||||
@@ -517,7 +450,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
|
||||
if (!this.props.data.delete) {
|
||||
controls.push(
|
||||
<Button kind="link" {...getHandleClickProps(this.deleteComment)} mix="comment__control">
|
||||
<Button kind="link" onClick={this.deleteComment} mix="comment__control">
|
||||
<FormattedMessage id="comment.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
);
|
||||
@@ -543,7 +476,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
const uploadImageHandler = this.isAnonymous() ? undefined : this.props.uploadImage;
|
||||
const commentControls = this.getCommentControls();
|
||||
const intl = props.intl;
|
||||
const CommentForm = this.props.CommentForm;
|
||||
const CommentForm = this.props.CommentForm || null;
|
||||
|
||||
/**
|
||||
* CommentType adapted for rendering
|
||||
@@ -574,13 +507,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
sign: !scoreSignEnabled ? '' : state.cachedScore > 0 ? '+' : state.cachedScore < 0 ? '−' : null,
|
||||
view: state.cachedScore > 0 ? 'positive' : state.cachedScore < 0 ? 'negative' : undefined,
|
||||
},
|
||||
user: {
|
||||
...props.data.user,
|
||||
picture:
|
||||
props.data.user.picture.indexOf(API_BASE) === 0
|
||||
? `${BASE_URL}${props.data.user.picture}`
|
||||
: props.data.user.picture,
|
||||
},
|
||||
user: props.data.user,
|
||||
};
|
||||
|
||||
const defaultMods = {
|
||||
@@ -646,6 +573,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
);
|
||||
}
|
||||
const goToParentMessage = intl.formatMessage(messages.goToParent);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={b('comment', { mix: this.props.mix }, defaultMods)}
|
||||
@@ -659,30 +587,25 @@ export class Comment extends Component<CommentProps, State> {
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__info">
|
||||
{props.view !== 'user' && !props.collapsed && <Avatar className="comment__avatar" url={o.user.picture} />}
|
||||
{props.view !== 'user' && !props.collapsed && (
|
||||
<div className="comment__avatar">
|
||||
<Avatar url={o.user.picture} />
|
||||
</div>
|
||||
)}
|
||||
{props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleUserInfoVisibility)}
|
||||
className="comment__username"
|
||||
title={o.user.id}
|
||||
>
|
||||
<button onClick={() => this.toggleUserInfoVisibility()} className="comment__username">
|
||||
{o.user.name}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleVerify)}
|
||||
aria-label={intl.formatMessage(messages.toggleVerification)}
|
||||
title={
|
||||
o.user.verified
|
||||
? intl.formatMessage(messages.verifiedUser)
|
||||
: intl.formatMessage(messages.unverifiedUser)
|
||||
}
|
||||
title={intl.formatMessage(o.user.verified ? messages.verifiedUser : messages.unverifiedUser)}
|
||||
className={b('comment__verification', {}, { active: o.user.verified, clickable: true })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
||||
<span
|
||||
title={intl.formatMessage(messages.verifiedUser)}
|
||||
@@ -774,7 +697,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
{(!props.collapsed || props.view === 'pinned') && (
|
||||
<div className="comment__actions">
|
||||
{!props.data.delete && !props.isCommentsDisabled && !props.disabled && props.view === 'main' && (
|
||||
<Button kind="link" {...getHandleClickProps(this.toggleReplying)} mix="comment__action">
|
||||
<Button kind="link" onClick={this.toggleReplying} mix="comment__action">
|
||||
{isReplying ? (
|
||||
<FormattedMessage id="comment.cancel" defaultMessage="Cancel" />
|
||||
) : (
|
||||
@@ -886,3 +809,70 @@ function getLocalDatetime(intl: IntlShape, date: Date) {
|
||||
time: intl.formatTime(date),
|
||||
});
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
deleteMessage: {
|
||||
id: 'comment.delete-message',
|
||||
defaultMessage: 'Do you want to delete this comment?',
|
||||
},
|
||||
hideUserComments: {
|
||||
id: 'comment.hide-user-comment',
|
||||
defaultMessage: 'Do you want to hide comments of {userName}?',
|
||||
},
|
||||
pinComment: {
|
||||
id: 'comment.pin-comment',
|
||||
defaultMessage: 'Do you want to pin this comment?',
|
||||
},
|
||||
unpinComment: {
|
||||
id: 'comment.unpin-comment',
|
||||
defaultMessage: 'Do you want to unpin this comment?',
|
||||
},
|
||||
verifyUser: {
|
||||
id: 'comment.verify-user',
|
||||
defaultMessage: 'Do you want to verify {userName}?',
|
||||
},
|
||||
unverifyUser: {
|
||||
id: 'comment.unverify-user',
|
||||
defaultMessage: 'Do you want to unverify {userName}?',
|
||||
},
|
||||
blockUser: {
|
||||
id: 'comment.block-user',
|
||||
defaultMessage: 'Do you want to block {userName} {duration}?',
|
||||
},
|
||||
unblockUser: {
|
||||
id: 'comment.unblock-user',
|
||||
defaultMessage: 'Do you want to unblock this user?',
|
||||
},
|
||||
deletedComment: {
|
||||
id: 'comment.deleted-comment',
|
||||
defaultMessage: 'This comment was deleted',
|
||||
},
|
||||
controversy: {
|
||||
id: 'comment.controversy',
|
||||
defaultMessage: 'Controversy: {value}',
|
||||
},
|
||||
toggleVerification: {
|
||||
id: 'comment.toggle-verification',
|
||||
defaultMessage: 'Toggle verification',
|
||||
},
|
||||
verifiedUser: {
|
||||
id: 'comment.verified-user',
|
||||
defaultMessage: 'Verified user',
|
||||
},
|
||||
unverifiedUser: {
|
||||
id: 'comment.unverified-user',
|
||||
defaultMessage: 'Unverified user',
|
||||
},
|
||||
goToParent: {
|
||||
id: 'comment.go-to-parent',
|
||||
defaultMessage: 'Go to parent comment',
|
||||
},
|
||||
expiredTime: {
|
||||
id: 'comment.expired-time',
|
||||
defaultMessage: 'Editing time has expired.',
|
||||
},
|
||||
commentTime: {
|
||||
id: 'comment.time',
|
||||
defaultMessage: '{day} at {time}',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import b from 'bem-react-helper';
|
||||
import { Theme } from 'common/types';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import { Button } from 'components/button';
|
||||
import { parseMessage } from 'utils/postMessage';
|
||||
import { parseMessage } from 'utils/post-message';
|
||||
|
||||
type Props = RenderableProps<{
|
||||
title: string;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
.root {
|
||||
display: inline-flex;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
padding: 4px;
|
||||
transition: transfrom 0.15s ease-out;
|
||||
border-radius: 2px;
|
||||
appearance: none;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.06);
|
||||
transition: transfrom 0.15s ease-in;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px rgba(var(--primary-color), 0.5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, RenderableProps, JSX } from 'preact';
|
||||
|
||||
import styles from './icon-button.module.css';
|
||||
|
||||
type Props = JSX.HTMLAttributes<HTMLButtonElement> & RenderableProps<{ className?: string }>;
|
||||
|
||||
export function IconButton({ title, children, className, ...props }: Props) {
|
||||
return (
|
||||
<button className={clsx('icon-button', className, styles.root)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { h, JSX } from 'preact';
|
||||
|
||||
type Props = Omit<JSX.SVGAttributes<SVGSVGElement>, 'size'> & {
|
||||
size?: number | string;
|
||||
};
|
||||
|
||||
export function CrossIcon({ size = 14, ...props }: Props) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M2 2L12 12M12 2L2 12"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { h, JSX } from 'preact';
|
||||
|
||||
type Props = Omit<JSX.SVGAttributes<SVGSVGElement>, 'size'> & {
|
||||
size?: number | string;
|
||||
};
|
||||
|
||||
export function SignOutIcon({ size = 16, ...props }: Props) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 16 16" fill="none" {...props}>
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
d="M5.7 15H2.6A1.6 1.6 0 011 13.4V2.6A1.6 1.6 0 012.6 1h3M11.1 11.9L15 8l-3.9-3.9M15 8H5.7"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -20,7 +20,6 @@ export function ListComments({ comments = [] }: Props) {
|
||||
<Comment
|
||||
intl={intl}
|
||||
key={comment.id}
|
||||
CommentForm={null}
|
||||
data={comment}
|
||||
level={0}
|
||||
view="preview"
|
||||
@@ -28,7 +27,6 @@ export function ListComments({ comments = [] }: Props) {
|
||||
user={null}
|
||||
theme="light"
|
||||
isCommentsDisabled={false}
|
||||
post_info={null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Preloader({ className }: Props) {
|
||||
return <div className={clsx('preloader', className)} aria-label="Loading..." />;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './preloader';
|
||||
|
||||
// all styles were moved to iframe.html
|
||||
@@ -1,10 +0,0 @@
|
||||
import { h } from 'preact';
|
||||
import b, { Mix } from 'bem-react-helper';
|
||||
|
||||
type Props = {
|
||||
mix?: Mix;
|
||||
};
|
||||
|
||||
export function Preloader({ mix }: Props) {
|
||||
return <div className={b('preloader', { mix })} />;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './profile';
|
||||
@@ -0,0 +1,170 @@
|
||||
.root {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.5s ease-out;
|
||||
|
||||
@media (min-width: 448px) {
|
||||
transform: translateX(448px);
|
||||
}
|
||||
}
|
||||
|
||||
.rootAppear {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.rootDisapear {
|
||||
transition: transform 0.5s ease-in;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
background: rgb(var(--primary-background-color));
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@media (min-width: 448px) {
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButtonWrapper {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
right: 10px;
|
||||
top: 18px;
|
||||
box-sizing: border-box;
|
||||
color: rgb(var(--secondary-text-color));
|
||||
|
||||
@media (min-width: 448px) {
|
||||
position: initial;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
box-sizing: border-box;
|
||||
color: rgb(var(--white-color));
|
||||
text-align: right; /* Position close button at right */
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-track {
|
||||
background: unset;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(var(--primary-background-color));
|
||||
border-radius: 5px;
|
||||
border: 2px solid rgb(var(--white-color));
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.info {
|
||||
max-width: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.id {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
color: var(--color13);
|
||||
}
|
||||
|
||||
.signout {
|
||||
margin-left: auto;
|
||||
margin-right: 28px;
|
||||
color: rgb(var(--secondary-text-color));
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@media (min-width: 448px) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
margin: 0 0 4px;
|
||||
padding-top: 12px 0;
|
||||
background-color: rgb(var(--primary-background-color));
|
||||
z-index: 1;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--primary-background-color), 0),
|
||||
rgba(var(--primary-background-color), 1)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.preloader {
|
||||
margin: 0 auto 18px;
|
||||
color: var(--color13);
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
margin: auto;
|
||||
color: var(--color13);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { h } from 'preact';
|
||||
import '@testing-library/jest-dom';
|
||||
import { waitFor } from '@testing-library/preact';
|
||||
|
||||
import { render } from 'tests/utils';
|
||||
import * as api from 'common/api';
|
||||
import * as pq from 'utils/parse-query';
|
||||
import type { Comment, User } from 'common/types';
|
||||
|
||||
import { Profile } from './profile';
|
||||
|
||||
const userParamsStub = {
|
||||
id: '1',
|
||||
name: 'username',
|
||||
picture: '/avatar.png',
|
||||
};
|
||||
|
||||
const userStub: User = {
|
||||
...userParamsStub,
|
||||
ip: '',
|
||||
picture: '/avatar.png',
|
||||
admin: false,
|
||||
block: false,
|
||||
verified: false,
|
||||
};
|
||||
|
||||
const commentStub: Comment = {
|
||||
id: '1',
|
||||
pid: '2',
|
||||
text: 'comment content',
|
||||
locator: {
|
||||
site: '',
|
||||
url: '',
|
||||
},
|
||||
score: 0,
|
||||
vote: 0,
|
||||
voted_ips: [],
|
||||
time: '2021-04-02T14:52:39.985281605-05:00',
|
||||
user: userStub,
|
||||
};
|
||||
const commentsStub = [commentStub, commentStub, commentStub];
|
||||
|
||||
describe('<Profile />', () => {
|
||||
it('should render preloader', () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub }));
|
||||
const { container } = render(<Profile />);
|
||||
|
||||
expect(container.querySelector('[aria-label="Loading..."]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render without comments', async () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub }));
|
||||
const getUserComments = jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: [] }));
|
||||
|
||||
const { getByText } = render(<Profile />);
|
||||
|
||||
await waitFor(() => expect(getUserComments).toHaveBeenCalledWith('1'));
|
||||
await waitFor(() => expect(getByText("Don't have comments yet")).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should render user with comments', async () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => userParamsStub);
|
||||
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub }));
|
||||
|
||||
const { getByText } = render(<Profile />);
|
||||
|
||||
await waitFor(() => expect(getByText('Recent comments')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shoud render current user without comments', async () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' }));
|
||||
|
||||
const { getByText, getByTitle } = render(<Profile />);
|
||||
|
||||
expect(getByTitle('Sign Out')).toBeInTheDocument();
|
||||
expect(getByText('Request my data removal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shoud render current user with comments', async () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub, current: '1' }));
|
||||
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub }));
|
||||
|
||||
const { getByText, getByTitle } = render(<Profile />);
|
||||
|
||||
expect(getByTitle('Sign Out')).toBeInTheDocument();
|
||||
expect(getByText('Request my data removal')).toBeInTheDocument();
|
||||
await waitFor(() => expect(getByText('My recent comments')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('should render user without footer', async () => {
|
||||
jest.spyOn(pq, 'parseQuery').mockImplementation(() => ({ ...userParamsStub }));
|
||||
jest.spyOn(api, 'getUserComments').mockImplementation(async () => ({ comments: commentsStub }));
|
||||
|
||||
const { container } = render(<Profile />);
|
||||
|
||||
expect(container.querySelector('profile-footer')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { getUserComments } from 'common/api';
|
||||
import { parseQuery } from 'utils/parse-query';
|
||||
import { requestDeletion } from 'utils/email';
|
||||
import { setStyles } from 'utils/set-dom-props';
|
||||
import { Avatar } from 'components/avatar';
|
||||
import { postMessageToParent } from 'utils/post-message';
|
||||
import { Comment } from 'components/comment';
|
||||
import { Preloader } from 'components/preloader';
|
||||
import { SignOutIcon } from 'components/icons/signout';
|
||||
import { logout } from 'components/auth/auth.api';
|
||||
import { Spinner } from 'components/spinner/spinner';
|
||||
import { CrossIcon } from 'components/icons/cross';
|
||||
import { IconButton } from 'components/icon-button/icon-button';
|
||||
import { Button } from 'components/auth/components/button';
|
||||
import { messages as authMessages } from 'components/auth/auth.messsages';
|
||||
import type { Comment as CommentType, Theme } from 'common/types';
|
||||
|
||||
import styles from './profile.module.css';
|
||||
|
||||
async function signout() {
|
||||
postMessageToParent({ profile: null, signout: true });
|
||||
await logout();
|
||||
}
|
||||
|
||||
// TODO: rewrite hide user logic and bring button to user profile
|
||||
export function Profile() {
|
||||
const intl = useIntl();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const user = useMemo(() => parseQuery(), []);
|
||||
const [error, setError] = useState(false);
|
||||
const [comments, setComments] = useState<CommentType[] | null>(null);
|
||||
const [isSigningOut, setSigningOut] = useState(false);
|
||||
|
||||
function handleClickClose() {
|
||||
const rootElement = rootRef.current;
|
||||
|
||||
rootElement.classList.remove(styles.rootAppear);
|
||||
rootElement.classList.add(styles.rootDisapear);
|
||||
// No need to unsubscribe because iframe will be destroyed
|
||||
rootElement.addEventListener('transitionend', () => {
|
||||
postMessageToParent({ profile: null });
|
||||
});
|
||||
}
|
||||
|
||||
async function handleClickLogout() {
|
||||
setSigningOut(true);
|
||||
await signout();
|
||||
setSigningOut?.(false);
|
||||
}
|
||||
|
||||
async function handleClickRequestRemoveData() {
|
||||
await requestDeletion();
|
||||
await signout();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUserComments(user.id)
|
||||
.then(({ comments }) => setComments(comments))
|
||||
.catch(() => setError(true));
|
||||
}, [user.id]);
|
||||
|
||||
useEffect(() => {
|
||||
const styles = { height: '100%', padding: 0 };
|
||||
|
||||
setStyles(document.documentElement, styles);
|
||||
setStyles(document.body, styles);
|
||||
|
||||
function handleKeydown(evt: KeyboardEvent): void {
|
||||
if (evt.code !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
|
||||
postMessageToParent({ profile: null });
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeydown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
rootRef.current.classList.add(styles.rootAppear);
|
||||
}, []);
|
||||
|
||||
if (!user.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCurrent = user.current === '1';
|
||||
const commentsJSX = comments?.length ? (
|
||||
<>
|
||||
<h3 className={clsx('profile-title', styles.title)}>
|
||||
{isCurrent ? (
|
||||
<FormattedMessage id="user.my-comments" defaultMessage="My recent comments" />
|
||||
) : (
|
||||
<FormattedMessage key="user.recent-comments" id="user.recent-comments" defaultMessage="Recent comments" />
|
||||
)}
|
||||
</h3>
|
||||
{comments.map((comment) => (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
user={null}
|
||||
intl={intl}
|
||||
data={comment}
|
||||
level={0}
|
||||
view="user"
|
||||
isCommentsDisabled={false}
|
||||
theme={(user.theme as Theme) || 'light'}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className={clsx('profile-emptyState', styles.emptyState)}>
|
||||
<FormattedMessage id="empty-state" defaultMessage="Don't have comments yet" />
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={clsx('profile', styles.root)} ref={rootRef}>
|
||||
{/* disable jsx-a11y/no-static-element-interactions and jsx-a11y/click-events-have-key-events */}
|
||||
{/* that's fine because inside of the element we have button that will throw all events and provide all of the interactions */}
|
||||
{/* eslint-disable-next-line */}
|
||||
<div className={clsx('profile-close-button-wrapper', styles.closeButtonWrapper)} onClick={handleClickClose}>
|
||||
<IconButton title={intl.formatMessage({ id: 'profile.close', defaultMessage: 'Close profile' })}>
|
||||
<CrossIcon size="16" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<aside className={clsx('profile-sidebar', isCurrent && 'profile_current', styles.sidebar)}>
|
||||
<header className={clsx('profile-header', styles.header)}>
|
||||
<div className={clsx('profile-avatar', styles.avatar)}>
|
||||
<Avatar data-testid="avatar" url={user.picture} />
|
||||
</div>
|
||||
<div className={clsx('profile-content', styles.info)}>
|
||||
<div className={clsx('profile-title', styles.name)}>{user.name}</div>
|
||||
<div className={clsx('profile-id', styles.id)}>{user.id}</div>
|
||||
</div>
|
||||
{isCurrent && (
|
||||
<button
|
||||
className={clsx('profile-signout', styles.signout)}
|
||||
title={intl.formatMessage(authMessages.signout)}
|
||||
onClick={handleClickLogout}
|
||||
disabled={isSigningOut}
|
||||
>
|
||||
{isSigningOut ? <Spinner /> : <SignOutIcon />}
|
||||
</button>
|
||||
)}
|
||||
</header>
|
||||
<section className={clsx('profile-content', styles.content)}>
|
||||
{error && (
|
||||
<p className={clsx('profile-error', styles.error)}>
|
||||
<FormattedMessage id="errors.0" defaultMessage="Something went wrong. Please try again a bit later." />
|
||||
</p>
|
||||
)}
|
||||
{comments === null ? <Preloader /> : commentsJSX}
|
||||
</section>
|
||||
{isCurrent ? (
|
||||
<footer className={clsx('profile-footer', styles.footer)}>
|
||||
<Button kind="link" size="sm" onClick={handleClickRequestRemoveData}>
|
||||
<FormattedMessage id="profile.request-to-delete-data" defaultMessage="Request my data removal" />
|
||||
</Button>
|
||||
</footer>
|
||||
) : // TODO: implement hidding user comments
|
||||
null}
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
.root {
|
||||
position: relative;
|
||||
font-family: 'PT Sans', Helvetica, Arial, sans-serif;
|
||||
|
||||
&::selection {
|
||||
background: var(--color42);
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import { h, Component, FunctionComponent, Fragment } from 'preact';
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import type { Sorting } from 'common/types';
|
||||
import 'styles/global.css';
|
||||
import type { StoreState } from 'store';
|
||||
import {
|
||||
COMMENT_NODE_CLASSNAME_PREFIX,
|
||||
MAX_SHOWN_ROOT_COMMENTS,
|
||||
THEMES,
|
||||
IS_MOBILE,
|
||||
LS_EMAIL_KEY,
|
||||
} from 'common/constants';
|
||||
import { COMMENT_NODE_CLASSNAME_PREFIX, MAX_SHOWN_ROOT_COMMENTS, THEMES, IS_MOBILE } from 'common/constants';
|
||||
import { maxShownComments, url } from 'common/settings';
|
||||
|
||||
import { StaticStore } from 'common/static-store';
|
||||
@@ -25,11 +19,14 @@ import {
|
||||
fetchBlockedUsers,
|
||||
hideUser,
|
||||
unhideUser,
|
||||
signout,
|
||||
} from 'store/user/actions';
|
||||
import { fetchComments, updateSorting, addComment, updateComment, unsetCommentMode } from 'store/comments/actions';
|
||||
import { setCommentsReadOnlyState } from 'store/post-info/actions';
|
||||
import { setTheme } from 'store/theme/actions';
|
||||
|
||||
// TODO: make this button as default for all cases and replace current `components/Button`
|
||||
import { Button } from 'components/auth/components/button';
|
||||
import { Preloader } from 'components/preloader';
|
||||
import { Settings } from 'components/settings';
|
||||
import { AuthPanel } from 'components/auth-panel';
|
||||
@@ -39,11 +36,10 @@ import { ConnectedComment as Comment } from 'components/comment/connected-commen
|
||||
import { uploadImage, getPreview } from 'common/api';
|
||||
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||
import { bindActions } from 'utils/actionBinder';
|
||||
import { postMessageToParent, parseMessage } from 'utils/postMessage';
|
||||
import { postMessageToParent, parseMessage } from 'utils/post-message';
|
||||
import { useActions } from 'hooks/useAction';
|
||||
import { setCollapse } from 'store/thread/actions';
|
||||
import { logout } from 'components/auth/auth.api';
|
||||
import { Button } from 'components/auth/components/button';
|
||||
import { Sorting } from 'common/types';
|
||||
|
||||
import styles from './root.module.css';
|
||||
|
||||
@@ -85,6 +81,7 @@ const boundActions = bindActions({
|
||||
updateComment,
|
||||
setCollapse,
|
||||
unsetCommentMode,
|
||||
signout,
|
||||
});
|
||||
|
||||
type Props = ReturnType<typeof mapStateToProps> & typeof boundActions & { intl: IntlShape };
|
||||
@@ -98,7 +95,7 @@ interface State {
|
||||
|
||||
const messages = defineMessages({
|
||||
pinnedComments: {
|
||||
id: `root.pinned-comments`,
|
||||
id: 'root.pinned-comments',
|
||||
defaultMessage: 'Pinned comments',
|
||||
},
|
||||
});
|
||||
@@ -125,12 +122,12 @@ export class Root extends Component<Props, State> {
|
||||
const userloading = this.props.fetchUser().finally(() => this.setState({ isUserLoading: false }));
|
||||
|
||||
Promise.all([userloading, this.props.fetchComments()]).finally(() => {
|
||||
postMessageToParent({ height: document.body.offsetHeight });
|
||||
setTimeout(this.checkUrlHash);
|
||||
window.addEventListener('hashchange', this.checkUrlHash);
|
||||
postMessageToParent({ height: document.body.offsetHeight });
|
||||
});
|
||||
|
||||
window.addEventListener('message', this.onMessage.bind(this));
|
||||
window.addEventListener('message', this.onMessage);
|
||||
}
|
||||
|
||||
changeSort = async (sort: Sorting) => {
|
||||
@@ -139,14 +136,6 @@ export class Root extends Component<Props, State> {
|
||||
await this.props.updateSorting(sort);
|
||||
};
|
||||
|
||||
logout = async () => {
|
||||
await logout();
|
||||
this.props.setUser();
|
||||
this.props.unsetCommentMode();
|
||||
localStorage.removeItem(LS_EMAIL_KEY);
|
||||
await this.props.fetchComments();
|
||||
};
|
||||
|
||||
checkUrlHash = (e: Event & { newURL: string }) => {
|
||||
const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash;
|
||||
|
||||
@@ -180,15 +169,19 @@ export class Root extends Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
onMessage(event: MessageEvent) {
|
||||
onMessage = (event: MessageEvent) => {
|
||||
const data = parseMessage(event);
|
||||
|
||||
if (data.signout === true) {
|
||||
this.props.signout(false);
|
||||
}
|
||||
|
||||
if (!data.theme || !THEMES.includes(data.theme)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.setTheme(data.theme);
|
||||
}
|
||||
};
|
||||
|
||||
onBlockedUsersShow = async () => {
|
||||
if (this.props.user && this.props.user.admin) {
|
||||
@@ -220,7 +213,7 @@ export class Root extends Component<Props, State> {
|
||||
|
||||
render(props: Props, { isUserLoading, commentsShown, isSettingsVisible }: State) {
|
||||
if (isUserLoading) {
|
||||
return <Preloader mix="root__preloader" />;
|
||||
return <Preloader className="root__preloader" />;
|
||||
}
|
||||
|
||||
const isCommentsDisabled = props.info.read_only!;
|
||||
@@ -234,7 +227,7 @@ export class Root extends Component<Props, State> {
|
||||
onSortChange={this.changeSort}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
postInfo={this.props.info}
|
||||
onSignOut={this.logout}
|
||||
signout={this.props.signout}
|
||||
onBlockedUsersShow={this.onBlockedUsersShow}
|
||||
onBlockedUsersHide={this.onBlockedUsersHide}
|
||||
onCommentsChangeReadOnlyMode={this.props.setCommentsReadOnlyState}
|
||||
@@ -315,7 +308,7 @@ export class Root extends Component<Props, State> {
|
||||
|
||||
{props.isCommentsLoading && (
|
||||
<div className="root__threads" role="list">
|
||||
<Preloader mix="root__preloader" />
|
||||
<Preloader className="root__preloader" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -333,10 +326,10 @@ const CopyrightLink = (title: string) => (
|
||||
);
|
||||
|
||||
/** Root component connected to redux */
|
||||
export const ConnectedRoot: FunctionComponent = () => {
|
||||
export function ConnectedRoot() {
|
||||
const intl = useIntl();
|
||||
const props = useSelector(mapStateToProps);
|
||||
const actions = useActions(boundActions);
|
||||
const intl = useIntl();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -362,4 +355,4 @@ export const ConnectedRoot: FunctionComponent = () => {
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
.root {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(var(--white-color), 0.2);
|
||||
border-right-color: rgb(var(--white-color));
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
import { h } from 'preact';
|
||||
import { messages } from 'components/auth/auth.messsages';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import styles from './Spinner.module.css';
|
||||
|
||||
export function Spinner() {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('spinner', styles.root)}
|
||||
role="presentation"
|
||||
aria-label={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.user-info__avatar {
|
||||
position: absolute;
|
||||
top: 17px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.user-info__id {
|
||||
margin: 0 0 18px;
|
||||
padding-left: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
color: var(--color13);
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.user-info__preloader {
|
||||
margin: 0 auto 18px;
|
||||
color: var(--color13);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.user-info__title {
|
||||
margin: 6px 0 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 20px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import './user-info.css';
|
||||
|
||||
import './__avatar/user-info__avatar.css';
|
||||
import './__id/user-info__id.css';
|
||||
import './__preloader/user-info__preloader.css';
|
||||
import './__title/user-info__title.css';
|
||||
|
||||
export { ConnectedUserInfo as UserInfo } from './user-info';
|
||||
@@ -1,39 +0,0 @@
|
||||
import { h, Fragment } from 'preact';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { Comment as CommentType } from 'common/types';
|
||||
|
||||
import { Comment } from 'components/comment';
|
||||
import { Preloader } from 'components/preloader';
|
||||
|
||||
type Props = {
|
||||
comments: CommentType[];
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export function LastCommentsList({ comments, isLoading }: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
if (isLoading) {
|
||||
return <Preloader mix="user-info__preloader" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{comments.map((comment) => (
|
||||
<Comment
|
||||
key={comment.id}
|
||||
CommentForm={null}
|
||||
intl={intl}
|
||||
data={comment}
|
||||
level={0}
|
||||
view="user"
|
||||
user={null}
|
||||
isCommentsDisabled={false}
|
||||
theme="light"
|
||||
post_info={null}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
.user-info {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow: inherit;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid var(--color41);
|
||||
border-radius: 2px;
|
||||
background: var(--color6);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { h, JSX, Component, FunctionComponent } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useIntl, defineMessages, FormattedMessage, IntlShape } from 'react-intl';
|
||||
|
||||
import { StoreState } from 'store';
|
||||
import { Comment } from 'common/types';
|
||||
import { fetchInfo } from 'store/user-info/actions';
|
||||
import { parseQuery } from 'utils/parseQuery';
|
||||
import { bindActions } from 'utils/actionBinder';
|
||||
import { postMessageToParent } from 'utils/postMessage';
|
||||
import { useActions } from 'hooks/useAction';
|
||||
import { Avatar } from 'components/avatar';
|
||||
|
||||
import { LastCommentsList } from './last-comments-list';
|
||||
|
||||
const boundActions = bindActions({ fetchInfo });
|
||||
|
||||
const messages = defineMessages({
|
||||
unexpectedError: {
|
||||
id: 'user-info.unexpected-error',
|
||||
defaultMessage: 'Something went wrong',
|
||||
},
|
||||
});
|
||||
|
||||
const user = parseQuery();
|
||||
|
||||
type Props = {
|
||||
comments: Comment[] | null;
|
||||
} & typeof boundActions & { intl: IntlShape };
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
class UserInfo extends Component<Props, State> {
|
||||
state = { isLoading: true, error: null };
|
||||
|
||||
componentWillMount(): void {
|
||||
if (!this.props.comments && this.state.isLoading) {
|
||||
this.props
|
||||
.fetchInfo(user.id)
|
||||
.then(() => {
|
||||
this.setState({ isLoading: false });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ isLoading: false, error: this.props.intl.formatMessage(messages.unexpectedError) });
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', UserInfo.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener('keydown', UserInfo.onKeyDown);
|
||||
}
|
||||
|
||||
render(): JSX.Element | null {
|
||||
const { comments = [] } = this.props;
|
||||
const { isLoading } = this.state;
|
||||
|
||||
// TODO: handle
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={b('root user-info', {})}>
|
||||
<Avatar className="user-info__avatar" url={user.picture} />
|
||||
<p className="user-info__title">
|
||||
<FormattedMessage
|
||||
id="user-info.last-comments"
|
||||
defaultMessage="Last comments by {userName}"
|
||||
values={{ userName: user.name }}
|
||||
/>
|
||||
</p>
|
||||
<p className="user-info__id">{user.id}</p>
|
||||
{!!comments && <LastCommentsList isLoading={isLoading} comments={comments} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Global on `keydown` handler which is set on component mount.
|
||||
* Listens for user's `esc` key press
|
||||
*/
|
||||
static onKeyDown(e: KeyboardEvent): void {
|
||||
// ESCAPE key pressed
|
||||
if (e.keyCode === 27) {
|
||||
postMessageToParent({ profile: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const commentsSelector = (state: StoreState) => state.userComments[user.id];
|
||||
|
||||
export const ConnectedUserInfo: FunctionComponent = () => {
|
||||
const comments = useSelector(commentsSelector);
|
||||
const actions = useActions(boundActions);
|
||||
const intl = useIntl();
|
||||
|
||||
return <UserInfo comments={comments} {...actions} intl={intl} />;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
export function Verified() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
|
||||
<path
|
||||
fill="#4fbbd6"
|
||||
d="M5.823 14.822l-1.28.206a.824.824 0 0 1-.9-.52l-.463-1.212a.824.824 0 0 0-.476-.476l-1.212-.462a.824.824 0 0 1-.52-.9l.206-1.281a.824.824 0 0 0-.175-.65L.185 8.52a.824.824 0 0 1 0-1.04l.818-1.006a.824.824 0 0 0 .175-.65L.972 4.542a.824.824 0 0 1 .52-.9l1.212-.463a.824.824 0 0 0 .476-.476l.462-1.212a.824.824 0 0 1 .9-.52l1.281.206a.824.824 0 0 0 .65-.175L7.48.185a.824.824 0 0 1 1.04 0l1.006.818a.824.824 0 0 0 .65.175l1.281-.206a.824.824 0 0 1 .9.52l.463 1.212c.084.22.257.392.476.476l1.212.462c.365.14.582.515.52.9l-.206 1.281a.824.824 0 0 0 .175.65l.818 1.007a.824.824 0 0 1 0 1.04l-.818 1.006a.824.824 0 0 0-.175.65l.206 1.281a.824.824 0 0 1-.52.9l-1.212.463a.824.824 0 0 0-.476.476l-.462 1.212a.824.824 0 0 1-.9.52l-1.281-.206a.824.824 0 0 0-.65.175l-1.007.818a.824.824 0 0 1-1.04 0l-1.006-.818a.824.824 0 0 0-.65-.175z"
|
||||
/>
|
||||
<path
|
||||
stroke="#fff"
|
||||
stroke-width="1.6"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4.755 8.252L7 10.5l4.495-4.495"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
:root {
|
||||
--color0: #0f172a;
|
||||
--color8: #262626;
|
||||
--color22: #2d2d2c;
|
||||
--color24: #313133;
|
||||
--color7: #333;
|
||||
--color23: #393734;
|
||||
--color18: #383838;
|
||||
--color19: #404040;
|
||||
--color36: #575757;
|
||||
--color34: #555;
|
||||
--color37: #586069;
|
||||
--color14: #6a6a6a;
|
||||
--color10: #777;
|
||||
--color13: #888;
|
||||
--color11: #969696;
|
||||
--color32: #a6a6a6;
|
||||
--color1: #aaa;
|
||||
--color35: #d1d5db;
|
||||
--color20: #ddd;
|
||||
--color46: rgba(27, 31, 35, 0.15);
|
||||
--color45: rgba(0, 0, 0, 0.1);
|
||||
--color41: #efefef;
|
||||
--color5: #eee;
|
||||
--color44: rgba(255, 255, 255, 0.3);
|
||||
--color6: #fff;
|
||||
--color3: #e2efef;
|
||||
--color4: #edf6f7;
|
||||
--color16: #e2e8f0;
|
||||
--color31: #cbd5e1;
|
||||
--color21: #f1f5f9;
|
||||
--color29: #0e7e9d;
|
||||
--color9: #0aa;
|
||||
--color15: #099;
|
||||
--color33: #06c5c5;
|
||||
--color40: #9cdddb;
|
||||
--color43: #b7dddd;
|
||||
--color42: #c6efef;
|
||||
--color48: rgba(37, 156, 154, 0.6);
|
||||
--color47: rgba(37, 156, 154, 0.4);
|
||||
--color12: #259e06;
|
||||
--color28: #672323;
|
||||
--color25: #9a0000;
|
||||
--color30: #cc0606;
|
||||
--color38: #ef0000;
|
||||
--color27: #f98989;
|
||||
--color26: #ffd7d7;
|
||||
|
||||
/* code-highlight */
|
||||
--chroma-bg: rgba(0, 0, 0, 0.05);
|
||||
--chroma-base: #586e75;
|
||||
--chroma-c: #7d7d7d;
|
||||
--chroma-01: #589000;
|
||||
--chroma-02: #cb4b16;
|
||||
--chroma-03: #268bd2;
|
||||
--chroma-04: #2aa198;
|
||||
--chroma-05: #859900;
|
||||
--chroma-06: #d33682;
|
||||
--chroma-07: #00aee2;
|
||||
|
||||
/* Named variables */
|
||||
--primary-color: 0, 170, 170;
|
||||
--primary-brighter-color: 0, 153, 153;
|
||||
--secondary-text-color: 100, 116, 139;
|
||||
--black-color: 0, 0, 0;
|
||||
--white-color: 255, 255, 255;
|
||||
--error-color: #b91c1c;
|
||||
--error-background: #ff466f2b;
|
||||
--line-color: var(--color16);
|
||||
--line-brighter-color: var(--color31);
|
||||
}
|
||||
|
||||
:root .dark {
|
||||
--line-color: var(--color36);
|
||||
--primary-color: 0, 153, 153;
|
||||
--primary-brighter-color: 0, 170, 170;
|
||||
--secondary-text-color: 209, 213, 219;
|
||||
--error-color: #ffa0a0;
|
||||
--line-brighter-color: var(--color11);
|
||||
}
|
||||
+29
-234
@@ -1,6 +1,8 @@
|
||||
import type { UserInfo, Theme } from 'common/types';
|
||||
import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config';
|
||||
import { parseMessage, postMessageToIframe } from 'utils/postMessage';
|
||||
import { NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants.config';
|
||||
import { parseMessage, postMessageToIframe } from 'utils/post-message';
|
||||
import { createIframe } from 'utils/create-iframe';
|
||||
import type { Theme } from 'common/types';
|
||||
import { closeProfile, openProfile } from 'profile';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -8,49 +10,6 @@ if (document.readyState === 'loading') {
|
||||
init();
|
||||
}
|
||||
|
||||
function removeDomNode(node: HTMLElement | null) {
|
||||
if (node && node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}
|
||||
|
||||
function createFrame({
|
||||
host,
|
||||
query,
|
||||
height,
|
||||
margin = '-6px',
|
||||
__colors__ = {},
|
||||
}: {
|
||||
host: string;
|
||||
query: string;
|
||||
height?: string;
|
||||
margin?: string;
|
||||
__colors__?: Record<string, string>;
|
||||
}) {
|
||||
const iframe = document.createElement('iframe');
|
||||
|
||||
iframe.src = `${host}/web/iframe.html?${query}`;
|
||||
iframe.name = JSON.stringify({ __colors__ });
|
||||
iframe.setAttribute('width', '100%');
|
||||
iframe.setAttribute('frameborder', '0');
|
||||
iframe.setAttribute('allowtransparency', 'true');
|
||||
iframe.setAttribute('scrolling', 'no');
|
||||
iframe.setAttribute('tabindex', '0');
|
||||
iframe.setAttribute('title', 'Comments | Remark42');
|
||||
iframe.setAttribute('horizontalscrolling', 'no');
|
||||
iframe.setAttribute('verticalscrolling', 'no');
|
||||
iframe.setAttribute(
|
||||
'style',
|
||||
`width: 1px !important; min-width: 100% !important; border: none !important; overflow: hidden !important; margin: ${margin};`
|
||||
);
|
||||
|
||||
if (height) {
|
||||
iframe.setAttribute('height', height);
|
||||
}
|
||||
|
||||
return iframe;
|
||||
}
|
||||
|
||||
function init() {
|
||||
window.REMARK42 = window.REMARK42 || {};
|
||||
window.REMARK42.createInstance = createInstance;
|
||||
@@ -73,32 +32,20 @@ function createInstance(config: typeof window.remark_config) {
|
||||
throw new Error('Remark42: Site ID is undefined.');
|
||||
}
|
||||
|
||||
let initDataAnimationTimeout: number | null = null;
|
||||
let titleObserver: MutationObserver | null = null;
|
||||
|
||||
config.url = (config.url || `${window.location.origin}${window.location.pathname}`).split('#')[0];
|
||||
|
||||
const query = Object.keys(config)
|
||||
.filter((key) => key !== '__colors__')
|
||||
.map(
|
||||
(key) =>
|
||||
`${encodeURIComponent(key)}=${encodeURIComponent(
|
||||
config[key as keyof Omit<typeof window.remark_config, '__colors__'>] as string | number | boolean
|
||||
)}`
|
||||
)
|
||||
.join('&');
|
||||
|
||||
const iframe =
|
||||
(root.firstElementChild as HTMLIFrameElement) ||
|
||||
createFrame({ host: BASE_URL, query, __colors__: config.__colors__ });
|
||||
const iframe = (root.firstElementChild as HTMLIFrameElement) || createIframe(config);
|
||||
|
||||
root.appendChild(iframe);
|
||||
|
||||
window.addEventListener('message', receiveMessages);
|
||||
window.addEventListener('hashchange', postHashToIframe);
|
||||
window.addEventListener('message', handleReceiveMessage);
|
||||
window.addEventListener('hashchange', handleHashChange);
|
||||
document.addEventListener('click', postClickOutsideToIframe);
|
||||
|
||||
const titleElement = document.querySelector('title');
|
||||
|
||||
if (titleElement) {
|
||||
titleObserver = new MutationObserver((mutations) => postTitleToIframe(mutations[0].target.textContent!));
|
||||
titleObserver.observe(titleElement, {
|
||||
@@ -108,198 +55,49 @@ function createInstance(config: typeof window.remark_config) {
|
||||
});
|
||||
}
|
||||
|
||||
const remarkRootId = 'remark-km423lmfdslkm34';
|
||||
const userInfo: {
|
||||
node: HTMLElement | null;
|
||||
back: HTMLElement | null;
|
||||
closeEl: HTMLElement | null;
|
||||
iframe: HTMLIFrameElement | null;
|
||||
style: HTMLStyleElement | null;
|
||||
init: (user: UserInfo) => void;
|
||||
close: () => void;
|
||||
delay: number | null;
|
||||
events: string[];
|
||||
onAnimationClose: () => void;
|
||||
onKeyDown: (e: KeyboardEvent) => void;
|
||||
animationStop: () => void;
|
||||
remove: () => void;
|
||||
} = {
|
||||
node: null,
|
||||
back: null,
|
||||
closeEl: null,
|
||||
iframe: null,
|
||||
style: null,
|
||||
init(user) {
|
||||
this.animationStop();
|
||||
if (!this.style) {
|
||||
this.style = document.createElement('style');
|
||||
this.style.setAttribute('rel', 'stylesheet');
|
||||
this.style.setAttribute('type', 'text/css');
|
||||
this.style.innerHTML = `
|
||||
#${remarkRootId}-node {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
transition: transform 0.4s ease-out;
|
||||
max-width: 100%;
|
||||
transform: translate(400px, 0);
|
||||
}
|
||||
#${remarkRootId}-node[data-animation] {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
#${remarkRootId}-back {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-out;
|
||||
}
|
||||
#${remarkRootId}-back[data-animation] {
|
||||
opacity: 1;
|
||||
}
|
||||
#${remarkRootId}-close {
|
||||
top: 0px;
|
||||
right: 400px;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
font-size: 25px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
border-color: transparent;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
margin-right: 4px;
|
||||
background-color: transparent;
|
||||
}
|
||||
@media all and (max-width: 430px) {
|
||||
#${remarkRootId}-close {
|
||||
right: 0px;
|
||||
font-size: 20px;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
if (!this.node) {
|
||||
this.node = document.createElement('div');
|
||||
this.node.id = `${remarkRootId}-node`;
|
||||
}
|
||||
if (!this.back) {
|
||||
this.back = document.createElement('div');
|
||||
this.back.id = `${remarkRootId}-back`;
|
||||
this.back.onclick = () => this.close();
|
||||
}
|
||||
if (!this.closeEl) {
|
||||
this.closeEl = document.createElement('button');
|
||||
this.closeEl.id = `${remarkRootId}-close`;
|
||||
this.closeEl.innerHTML = '✖';
|
||||
this.closeEl.onclick = () => this.close();
|
||||
}
|
||||
const queryUserInfo = `${query}&page=user-info&&id=${user.id}&name=${user.name}&picture=${user.picture || ''}`;
|
||||
const iframe = createFrame({ host: BASE_URL, query: queryUserInfo, height: '100%', margin: '0' });
|
||||
this.node.appendChild(iframe);
|
||||
this.iframe = iframe;
|
||||
this.node.appendChild(this.closeEl);
|
||||
document.body.appendChild(this.style);
|
||||
document.body.appendChild(this.back);
|
||||
document.body.appendChild(this.node);
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
initDataAnimationTimeout = window.setTimeout(() => {
|
||||
this.back!.setAttribute('data-animation', '');
|
||||
this.node!.setAttribute('data-animation', '');
|
||||
iframe.focus();
|
||||
}, 400);
|
||||
},
|
||||
close() {
|
||||
if (this.node) {
|
||||
if (this.iframe) {
|
||||
this.node.removeChild(this.iframe);
|
||||
}
|
||||
this.onAnimationClose();
|
||||
this.node.removeAttribute('data-animation');
|
||||
}
|
||||
if (this.back) {
|
||||
this.back.removeAttribute('data-animation');
|
||||
}
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
},
|
||||
delay: null,
|
||||
events: ['', 'webkit', 'moz', 'MS', 'o'].map((prefix) => (prefix ? `${prefix}TransitionEnd` : 'transitionend')),
|
||||
onAnimationClose() {
|
||||
const el = this.node!;
|
||||
if (!this.node) {
|
||||
return;
|
||||
}
|
||||
this.delay = window.setTimeout(this.animationStop, 1000);
|
||||
this.events.forEach((event) => el.addEventListener(event, this.animationStop, false));
|
||||
},
|
||||
onKeyDown(e) {
|
||||
// ESCAPE key pressed
|
||||
if (e.keyCode === 27) {
|
||||
userInfo.close();
|
||||
}
|
||||
},
|
||||
animationStop() {
|
||||
const t = userInfo;
|
||||
if (!t.node) {
|
||||
return;
|
||||
}
|
||||
if (t.delay) {
|
||||
clearTimeout(t.delay);
|
||||
t.delay = null;
|
||||
}
|
||||
t.events.forEach((event) => t.node!.removeEventListener(event, t.animationStop, false));
|
||||
return t.remove();
|
||||
},
|
||||
remove() {
|
||||
const t = userInfo;
|
||||
removeDomNode(t.node);
|
||||
removeDomNode(t.back);
|
||||
removeDomNode(t.style);
|
||||
},
|
||||
};
|
||||
|
||||
function receiveMessages(event: MessageEvent): void {
|
||||
function handleReceiveMessage(event: MessageEvent): void {
|
||||
const data = parseMessage(event);
|
||||
|
||||
if (data.height) {
|
||||
if (typeof data.height === 'number') {
|
||||
iframe.style.height = `${data.height}px`;
|
||||
}
|
||||
|
||||
if (data.scrollTo) {
|
||||
if (typeof data.scrollTo === 'number') {
|
||||
window.scrollTo(window.pageXOffset, data.scrollTo + iframe.getBoundingClientRect().top + window.pageYOffset);
|
||||
}
|
||||
|
||||
if (typeof data.profile === 'object') {
|
||||
if (data.profile === null) {
|
||||
userInfo.close();
|
||||
closeProfile();
|
||||
} else {
|
||||
userInfo.init(data.profile);
|
||||
openProfile({ ...config, ...data.profile });
|
||||
}
|
||||
}
|
||||
|
||||
if (data.inited) {
|
||||
if (data.signout === true) {
|
||||
postMessageToIframe(iframe, { signout: true });
|
||||
}
|
||||
|
||||
if (data.inited === true) {
|
||||
postHashToIframe();
|
||||
postTitleToIframe(document.title);
|
||||
}
|
||||
}
|
||||
|
||||
function postHashToIframe(evt?: Event & { newURL: string }) {
|
||||
const hash = evt ? `#${evt.newURL.split('#')[1]}` : window.location.hash;
|
||||
|
||||
function postHashToIframe(hash = window.location.hash) {
|
||||
if (!hash.startsWith(`#${COMMENT_NODE_CLASSNAME_PREFIX}`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt?.preventDefault();
|
||||
postMessageToIframe(iframe, { hash });
|
||||
}
|
||||
|
||||
function handleHashChange(evt: HashChangeEvent) {
|
||||
const url = new URL(evt.newURL);
|
||||
|
||||
postHashToIframe(url.hash);
|
||||
}
|
||||
|
||||
function postTitleToIframe(title: string) {
|
||||
postMessageToIframe(iframe, { title });
|
||||
}
|
||||
@@ -312,16 +110,13 @@ function createInstance(config: typeof window.remark_config) {
|
||||
}
|
||||
|
||||
function changeTheme(theme: Theme) {
|
||||
window.remark_config.theme = theme;
|
||||
postMessageToIframe(iframe, { theme });
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
if (initDataAnimationTimeout) {
|
||||
clearTimeout(initDataAnimationTimeout);
|
||||
}
|
||||
|
||||
window.removeEventListener('message', receiveMessages);
|
||||
window.removeEventListener('hashchange', postHashToIframe);
|
||||
window.removeEventListener('message', handleReceiveMessage);
|
||||
window.removeEventListener('hashchange', handleHashChange);
|
||||
document.removeEventListener('click', postClickOutsideToIframe);
|
||||
|
||||
if (titleObserver) {
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Загрузка...",
|
||||
"auth.oauth-button": "Увайсці праз {provider}",
|
||||
"auth.oauth-source": "Скарыстацца сацыяльнай сеткай",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "або",
|
||||
"auth.signin": "Увайсці",
|
||||
"auth.signout": "Выйсці?",
|
||||
"auth.submit": "Адправіць",
|
||||
"auth.symbols-restriction": "імя карыстальніка мусіць пачынацца з літары і ўтрымоўваць толькі лацінскія літары, лічбы, знакі падкрэслівання і прабелы(?)",
|
||||
"auth.user-not-found": "Карыстальнік не знойдзены",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Уключыць каментары",
|
||||
"authPanel.enable-cookies": "Дазвольце кукі, каб увайсці і каментаваць",
|
||||
"authPanel.hide-settings": "Схаваць налады",
|
||||
"authPanel.logged-as": "Вы ўвайшлі як",
|
||||
"authPanel.logout": "Выйсці?",
|
||||
"authPanel.new-page": "новая старонка",
|
||||
"authPanel.read-only": "Толькі для чытання",
|
||||
"authPanel.request-to-delete-data": "Запытаць выдаленне маіх даных",
|
||||
"authPanel.show-settings": "Паказаць налады",
|
||||
"blockingDuration.day": "На дзень",
|
||||
"blockingDuration.month": "На месяц",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Старыя",
|
||||
"commentsSort.recently-updated": "Нядаўна абноўленыя",
|
||||
"commentsSort.worst": "Горшыя",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Нешта пайшло не так. Калі ласка, паспрабуйце яшчэ раз крыху пазней.",
|
||||
"errors.1": "Каментар не знойдзены. Калі ласка, абнавіце старонку і паспрабуйце зноў.",
|
||||
"errors.10": "Час рэдагавання каментара сышоў.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Запытаны файл не знойдзены.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Не атрымалася апрацаваць адказ сервера.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Вы не маеце дазволу для гэтага дзеяння.",
|
||||
"errors.4": "Няправільна адфарматаваны каментар.",
|
||||
"errors.5": "Каментар не знойдзены. Калі ласка, абнавіце старонку і паспрабуйце яшчэ раз.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Не аўтарызаваны.",
|
||||
"errors.to-many-request": "Зашмат запытаў.",
|
||||
"errors.unexpected-error": "Нешта пайшло не так.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Замацаваныя каментары",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Паказаць яшчэ",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Нумараваны спіс",
|
||||
"toolbar.quote": "Цытата",
|
||||
"toolbar.unordered-list": "Ненумараваны спіс",
|
||||
"user-info.last-comments": "Апошнія каментары {userName}",
|
||||
"user-info.unexpected-error": "Нешта пайшло не так",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Ананімныя карыстальнікі не могуць галасаваць",
|
||||
"vote.deleted": "Нельга галасаваць за выдалены каментар",
|
||||
"vote.guest": "Увайдзіце ў сістэму, каб галасаваць",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Зареждане...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "или",
|
||||
"auth.signin": "Вход",
|
||||
"auth.signout": "Изход",
|
||||
"auth.submit": "Изпрати",
|
||||
"auth.symbols-restriction": "Потребителското име трябва да започва с буква и да бъде само от латински букви, цифри, подчертавки или разтояния",
|
||||
"auth.user-not-found": "Не бе намерен потребител",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Разреши коментарите",
|
||||
"authPanel.enable-cookies": "Разреши бисквитките за вход и коментар",
|
||||
"authPanel.hide-settings": "Скрий настройките",
|
||||
"authPanel.logged-as": "Вие влязохте като",
|
||||
"authPanel.logout": "Изход?",
|
||||
"authPanel.new-page": "нова страница",
|
||||
"authPanel.read-only": "Само за четене",
|
||||
"authPanel.request-to-delete-data": "Заявка за премахване на моите данни",
|
||||
"authPanel.show-settings": "Покажи настройките",
|
||||
"blockingDuration.day": "За един ден",
|
||||
"blockingDuration.month": "За един месец",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Най-стар",
|
||||
"commentsSort.recently-updated": "Най-скоро променен",
|
||||
"commentsSort.worst": "Най-лошия",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Има някакъв проблем. Моля, опитайте отново по-късно.",
|
||||
"errors.1": "Коментара не бе намерен. Моля презаредете страницата и опитайте пак.",
|
||||
"errors.10": "Твърде късно е за редактиране на коментара.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Файла не бе намерен.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Неуспешно премахване на входящата заявка.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Нямате привилегия за тази операция.",
|
||||
"errors.4": "Невалидни данни на коментара.",
|
||||
"errors.5": "Коментара не бе намерен. Моля презаредете странцата и опитайте пак.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Неоторизиран.",
|
||||
"errors.to-many-request": "Достигнахте лимита на заявки.",
|
||||
"errors.unexpected-error": "Има някакъв проблем.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Фиксирани коментари",
|
||||
"root.powered-by": "Използва <a>Remark42</a>",
|
||||
"root.show-more": "Покажи още",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Добави номериран списък",
|
||||
"toolbar.quote": "Добави цитат",
|
||||
"toolbar.unordered-list": "Добави обозначен списък",
|
||||
"user-info.last-comments": "Последни коментари от {userName}",
|
||||
"user-info.unexpected-error": "Има някакъв проблем",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Анонимни потребители не могат да гласуват",
|
||||
"vote.deleted": "Не може да гласъвате за изтрит коментарт",
|
||||
"vote.guest": "Влезте за да гласувате",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Carregando...",
|
||||
"auth.oauth-button": "Entrar com {provider}",
|
||||
"auth.oauth-source": "Use redes sociais",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "ou",
|
||||
"auth.signin": "Entrar",
|
||||
"auth.signout": "Fazer logout",
|
||||
"auth.submit": "Enviar",
|
||||
"auth.symbols-restriction": "O nome de usuário deve conter apenas letras, números, sublinhados ou espaços",
|
||||
"auth.user-not-found": "Nenhum usuário encontrado",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Habilitar comentários",
|
||||
"authPanel.enable-cookies": "Permita cookies para fazer login e comentar",
|
||||
"authPanel.hide-settings": "Ocultar configurações",
|
||||
"authPanel.logged-as": "Você fez login como",
|
||||
"authPanel.logout": "Fazer logout?",
|
||||
"authPanel.new-page": "nova página",
|
||||
"authPanel.read-only": "Somente leitura",
|
||||
"authPanel.request-to-delete-data": "Solicitar remoção dos meus dados",
|
||||
"authPanel.show-settings": "Mostrar configurações",
|
||||
"blockingDuration.day": "Por um dia",
|
||||
"blockingDuration.month": "Por um mês",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Mais antigo",
|
||||
"commentsSort.recently-updated": "Atualizado recentemente",
|
||||
"commentsSort.worst": "Pior",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Ocorreu um erro. Tente novamente mais tarde.",
|
||||
"errors.1": "O comentário não pode ser encontrado. Atualize a página e tente novamente.",
|
||||
"errors.10": "É muito tarde para editar o comentário.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "O arquivo solicitado não foi encontrado.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Falha ao fazer unmarshalling da solicitação de entrada.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Você não tem permissão para esta operação.",
|
||||
"errors.4": "Dados de comentário inválidos.",
|
||||
"errors.5": "O comentário não pode ser encontrado. Atualize a página e tente novamente.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Não autorizado.",
|
||||
"errors.to-many-request": "Você atingiu o limite máximo de solicitações.",
|
||||
"errors.unexpected-error": "Algo deu errado.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Comentários afixados",
|
||||
"root.powered-by": "Com a tecnologia <a>Remark42</a>",
|
||||
"root.show-more": "Mostrar mais",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Adicionar uma lista numerada",
|
||||
"toolbar.quote": "Inserir citação",
|
||||
"toolbar.unordered-list": "Adicionar lista com marcadores",
|
||||
"user-info.last-comments": "Últimos comentários de {userName}",
|
||||
"user-info.unexpected-error": "Algo deu errado",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Usuários anônimos não podem votar",
|
||||
"vote.deleted": "Não posso votar em um comentário excluído",
|
||||
"vote.guest": "Faça login para votar",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Wird geladen ...",
|
||||
"auth.oauth-button": "Mit {provider} anmelden",
|
||||
"auth.oauth-source": "Soziales Netzwerk verwenden",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "oder",
|
||||
"auth.signin": "Einloggen",
|
||||
"auth.signout": "Abmelden",
|
||||
"auth.submit": "Absenden",
|
||||
"auth.symbols-restriction": "Der Benutzername darf nur Buchstaben, Zahlen, Unterstriche oder Leerzeichen enthalten",
|
||||
"auth.user-not-found": "Benutzer konnte nicht gefunden werden",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Kommentarfunktion aktivieren",
|
||||
"authPanel.enable-cookies": "Cookies für die Anmeldung und das Kommentieren zulassen",
|
||||
"authPanel.hide-settings": "Einstellungen ausblenden",
|
||||
"authPanel.logged-as": "Sie haben sich angemeldet als",
|
||||
"authPanel.logout": "Abmelden?",
|
||||
"authPanel.new-page": "neue Seite",
|
||||
"authPanel.read-only": "Schreibgeschützt",
|
||||
"authPanel.request-to-delete-data": "Löschung meiner Daten anfordern",
|
||||
"authPanel.show-settings": "Einstellungen anzeigen",
|
||||
"blockingDuration.day": "Für einen Tag",
|
||||
"blockingDuration.month": "Für einen Monat",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Älteste",
|
||||
"commentsSort.recently-updated": "Kürzlich aktualisierte",
|
||||
"commentsSort.worst": "Schlechteste",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Leider ist etwas schiefgegangen. Bitte versuchen Sie es später erneut.",
|
||||
"errors.1": "Kommentar nicht gefunden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.",
|
||||
"errors.10": "Das Zeitfenster für das Bearbeiten des Kommentars ist verstrichen.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Die angeforderte Datei wurde nicht gefunden.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Die eingehende Anfrage konnte nicht verarbeitet werden.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Für diesen Vorgang haben Sie keine ausreichende Berechtigung.",
|
||||
"errors.4": "Ungültige Kommentardaten.",
|
||||
"errors.5": "Kommentar nicht gefunden. Bitte laden Sie die Seite neu und versuchen Sie es erneut.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Nicht autorisiert.",
|
||||
"errors.to-many-request": "Sie haben das maximale Anfragelimit erreicht.",
|
||||
"errors.unexpected-error": "Leider ist etwas schiefgegangen.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Angeheftete Kommentare",
|
||||
"root.powered-by": "Unterstützt von <a>Remark42</a>",
|
||||
"root.show-more": "Mehr anzeigen",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Nummerierte Liste einfügen",
|
||||
"toolbar.quote": "Zitat einfügen",
|
||||
"toolbar.unordered-list": "Aufzählungsliste einfügen",
|
||||
"user-info.last-comments": "Kommentare von Benutzer {userName}",
|
||||
"user-info.unexpected-error": "Leider ist etwas schiefgegangen.",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Anonyme Benutzer können leider nicht abstimmen",
|
||||
"vote.deleted": "Sie können nicht für einen gelöschten Kommentar abstimmen",
|
||||
"vote.guest": "Melden Sie sich an, um abzustimmen",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Loading...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "or",
|
||||
"auth.signin": "Sign In",
|
||||
"auth.signout": "Sign Out",
|
||||
"auth.submit": "Submit",
|
||||
"auth.symbols-restriction": "Username must contain only letters, numbers, underscores or spaces",
|
||||
"auth.user-not-found": "No user was found",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Enable comments",
|
||||
"authPanel.enable-cookies": "Allow cookies to login and comment",
|
||||
"authPanel.hide-settings": "Hide settings",
|
||||
"authPanel.logged-as": "You logged in as",
|
||||
"authPanel.logout": "Logout?",
|
||||
"authPanel.new-page": "new page",
|
||||
"authPanel.read-only": "Read-only",
|
||||
"authPanel.request-to-delete-data": "Request my data removal",
|
||||
"authPanel.show-settings": "Show settings",
|
||||
"blockingDuration.day": "For a day",
|
||||
"blockingDuration.month": "For a month",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Oldest",
|
||||
"commentsSort.recently-updated": "Recently updated",
|
||||
"commentsSort.worst": "Worst",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Something went wrong. Please try again a bit later.",
|
||||
"errors.1": "Comment cannot be found. Please refresh the page and try again.",
|
||||
"errors.10": "It is too late to edit the comment.",
|
||||
@@ -94,18 +94,21 @@
|
||||
"errors.18": "Requested file cannot be found.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Failed to unmarshal incoming request.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "You don't have permission for this operation.",
|
||||
"errors.4": "Invalid comment data.",
|
||||
"errors.5": "Comment cannot be found. Please refresh the page and try again.",
|
||||
"errors.6": "Site cannot be found. Please refresh the page and try again.",
|
||||
"errors.7": "User has been blocked.",
|
||||
"errors.8": "User has been blocked.",
|
||||
"errors.8": "Can't post comments on this page. Comments are read only.",
|
||||
"errors.9": "Comment changing failed. Please try again a bit later.",
|
||||
"errors.failed-fetch": "Failed to fetch. Please check your internet connection or try again a bit later",
|
||||
"errors.forbidden": "Forbidden.",
|
||||
"errors.not-authorized": "Not authorized.",
|
||||
"errors.to-many-request": "You have reached maximum request limit.",
|
||||
"errors.unexpected-error": "Something went wrong.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Pinned comments",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Show more",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Add a numbered list",
|
||||
"toolbar.quote": "Insert a quote",
|
||||
"toolbar.unordered-list": "Add a bulleted list",
|
||||
"user-info.last-comments": "Last comments by {userName}",
|
||||
"user-info.unexpected-error": "Something went wrong",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Anonymous users can't vote",
|
||||
"vote.deleted": "Can't vote for deleted comment",
|
||||
"vote.guest": "Sign in to vote",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Cargando...",
|
||||
"auth.oauth-button": "Inicie sesión con {provider}",
|
||||
"auth.oauth-source": "Acceda a través de sus redes sociales",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "o",
|
||||
"auth.signin": "Acceder",
|
||||
"auth.signout": "¿Salir?",
|
||||
"auth.submit": "Enviar",
|
||||
"auth.symbols-restriction": "El nombre de usuario debe comenzar con una letra y contener solamente letras latinas, números, guión bajo o espacio",
|
||||
"auth.user-not-found": "No se encontró el usuario",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Habilitar comentarios",
|
||||
"authPanel.enable-cookies": "Habilitar cookies para acceder y comentar",
|
||||
"authPanel.hide-settings": "Ocultar opciones",
|
||||
"authPanel.logged-as": "Accediste como",
|
||||
"authPanel.logout": "¿Salir?",
|
||||
"authPanel.new-page": "nueva página",
|
||||
"authPanel.read-only": "Solo lectura",
|
||||
"authPanel.request-to-delete-data": "Solicitar la eliminación de mis datos",
|
||||
"authPanel.show-settings": "Mostrar opciones",
|
||||
"blockingDuration.day": "Por un día",
|
||||
"blockingDuration.month": "Por un mes",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Más antiguo",
|
||||
"commentsSort.recently-updated": "Actualizado más recientemente",
|
||||
"commentsSort.worst": "Peor",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Algo salió mal. Por favor vuelve a intentar más tarde.",
|
||||
"errors.1": "No se ha encontrado el comentario. Por favor refresca la página y vuelve a intentar.",
|
||||
"errors.10": "Es muy tarde para editar el comentario.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "No se ha encontrado el archivo solicitado.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "No se ha podido deserializar la petición entrante.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "No tienes permisos para esta operación.",
|
||||
"errors.4": "Datos de comentario inválidos.",
|
||||
"errors.5": "El comentario no se ha encontrado. Por favor refresca la página y vuelve a intentar.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "No autorizado.",
|
||||
"errors.to-many-request": "Has llegado al límite de peticiones.",
|
||||
"errors.unexpected-error": "Algo salió mal.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Comentarios anclados",
|
||||
"root.powered-by": "Con la tecnología de <a>Remark42</a>",
|
||||
"root.show-more": "Mostrar más",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Agrega una lista numerada",
|
||||
"toolbar.quote": "Inserta una cita",
|
||||
"toolbar.unordered-list": "Agrega una lista sin numerar",
|
||||
"user-info.last-comments": "Últimos comentarios de {userName}",
|
||||
"user-info.unexpected-error": "Algo salió mal",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Los usuarios anónimos no puede votar",
|
||||
"vote.deleted": "No se puede votar un comentario eliminado",
|
||||
"vote.guest": "Accede para votar",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Ladataan...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "tai",
|
||||
"auth.signin": "Kirjaudu sisään",
|
||||
"auth.signout": "Kirjaudu ulos",
|
||||
"auth.submit": "Lähetä",
|
||||
"auth.symbols-restriction": "Käyttäjätunnuksen tulee alkaa kirjaimella ja sisältää vain latinalaisia kirjaimia, numeroita, alaviivoja ja välilyöntejä",
|
||||
"auth.user-not-found": "Käyttäjää ei löytynyt",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Ota kommentit käyttöön",
|
||||
"authPanel.enable-cookies": "Salli evästeet kirjautuaksesi sisään ja kommentoidaksesi",
|
||||
"authPanel.hide-settings": "Piilota asetukset",
|
||||
"authPanel.logged-as": "Olet kirjautunut sisään nimellä",
|
||||
"authPanel.logout": "Kirjaudu ulos?",
|
||||
"authPanel.new-page": "uusi sivu",
|
||||
"authPanel.read-only": "Vain luku",
|
||||
"authPanel.request-to-delete-data": "Pyydä tietojen poistamista",
|
||||
"authPanel.show-settings": "Näytä asetukset",
|
||||
"blockingDuration.day": "Päiväksi",
|
||||
"blockingDuration.month": "Kuukaudeksi",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Vanhin",
|
||||
"commentsSort.recently-updated": "Äskettäin päivitetty",
|
||||
"commentsSort.worst": "Huonoin",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Jotain meni pieleen. Yritä uudelleen myöhemmin.",
|
||||
"errors.1": "Kommenttia ei löydy. Päivitä sivu ja yritä uudelleen.",
|
||||
"errors.10": "Aikaikkuna kommentin muokkaamiseen on ohitettu.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Pyydettyä tiedostoa ei löydy.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Failed to unmarshal incoming request.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Sinulla ei ole lupaa tähän operaatioon.",
|
||||
"errors.4": "Virheellinen kommentti.",
|
||||
"errors.5": "Kommenttia ei löydy. Päivitä sivu ja yritä uudelleen.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Ei sallittu.",
|
||||
"errors.to-many-request": "Liian monta pyyntöä.",
|
||||
"errors.unexpected-error": "Jotain meni pieleen.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Kiinnitetyt kommentit",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Näytä lisää",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Järjestetty luettelo",
|
||||
"toolbar.quote": "Lainaus",
|
||||
"toolbar.unordered-list": "Järjestämätön luettelo",
|
||||
"user-info.last-comments": "Uusimmat kommentit käyttäjältä {userName}",
|
||||
"user-info.unexpected-error": "Jotain meni pieleen",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Anonyymit käyttäjät eivät voi äänestää",
|
||||
"vote.deleted": "Poistettua kommenttia ei voi äänestää",
|
||||
"vote.guest": "Kirjaudu sisään äänestääksesi",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Chargement...",
|
||||
"auth.oauth-button": "Se connecter avec {provider}",
|
||||
"auth.oauth-source": "Utiliser les réseaux sociaux",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "ou",
|
||||
"auth.signin": "Se connecter",
|
||||
"auth.signout": "Se déconnecter",
|
||||
"auth.submit": "Valider",
|
||||
"auth.symbols-restriction": "Le nom d'utilisateur ne doit contenir que des lettres, nombres, tirets bas et espaces.",
|
||||
"auth.user-not-found": "Aucun utilisateur n'a été trouvé",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Permettre les commentaires",
|
||||
"authPanel.enable-cookies": "Activez les cookies pour vous connecter et commenter",
|
||||
"authPanel.hide-settings": "Cacher les paramètres",
|
||||
"authPanel.logged-as": "Vous êtes connecté en tant que",
|
||||
"authPanel.logout": "Se déconnecter ?",
|
||||
"authPanel.new-page": "nouvelle page",
|
||||
"authPanel.read-only": "Lecture seule",
|
||||
"authPanel.request-to-delete-data": "Demander la suppression de mes données",
|
||||
"authPanel.show-settings": "Afficher les paramètres",
|
||||
"blockingDuration.day": "Pour un jour",
|
||||
"blockingDuration.month": "Pour un mois",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Le plus ancien",
|
||||
"commentsSort.recently-updated": "Mis à jour récemment",
|
||||
"commentsSort.worst": "Le pire",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Une erreur s'est produite. Veuillez réessayer un peu plus tard.",
|
||||
"errors.1": "Commentaire introuvable. Rafraichissez la page et réessayez.",
|
||||
"errors.10": "Il n'est plus possible de modifier ce commentaire.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Le fichier demandé est introuvable.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Échec du traitement de la requête entrante.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Vous n'avez pas l'autorisation d'effectuer cette opération.",
|
||||
"errors.4": "Données de commentaire non valides.",
|
||||
"errors.5": "Commentaire introuvable. Rafraichissez la page et réessayez.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Non autorisé.",
|
||||
"errors.to-many-request": "Vous avez atteint la limite maximale de requêtes.",
|
||||
"errors.unexpected-error": "Un problème est survenu.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Commentaires épinglés",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Afficher plus",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Ajouter une liste numérotée",
|
||||
"toolbar.quote": "Insérer une citation",
|
||||
"toolbar.unordered-list": "Ajouter une liste à puces",
|
||||
"user-info.last-comments": "Derniers commentaires par {userName}",
|
||||
"user-info.unexpected-error": "Un problème est survenu",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Les utilisateurs anonymes ne peuvent pas voter",
|
||||
"vote.deleted": "Vous ne pouvez pas voter pour un commentaire supprimé",
|
||||
"vote.guest": "Connectez-vous pour voter",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "読み込んでいます…",
|
||||
"auth.oauth-button": "{provider} でサインイン",
|
||||
"auth.oauth-source": "ソーシャルネットワークを使用",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "または",
|
||||
"auth.signin": "サインイン",
|
||||
"auth.signout": "ログアウトしますか",
|
||||
"auth.submit": "送信",
|
||||
"auth.symbols-restriction": "ユーザー名には英数字、アンダースコア、またはスペースのみを指定してください",
|
||||
"auth.user-not-found": "ユーザーが見つかりませんでした",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "コメントの有効化",
|
||||
"authPanel.enable-cookies": "Cookie を許可してログインとコメントを有効にしてください",
|
||||
"authPanel.hide-settings": "設定を非表示",
|
||||
"authPanel.logged-as": "次でログイン中:",
|
||||
"authPanel.logout": "ログアウトしますか?",
|
||||
"authPanel.new-page": "新規ページ",
|
||||
"authPanel.read-only": "読み取り専用",
|
||||
"authPanel.request-to-delete-data": "データ除去の依頼",
|
||||
"authPanel.show-settings": "設定を表示",
|
||||
"blockingDuration.day": "1 日",
|
||||
"blockingDuration.month": "1 カ月",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "古い順",
|
||||
"commentsSort.recently-updated": "更新が最も新しい順",
|
||||
"commentsSort.worst": "最悪",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "不明なエラーが発生しました。しばらくしてからもう一度お試しください。",
|
||||
"errors.1": "コメントが見つかりません。ページを再読み込みしてからもう一度お試しください。",
|
||||
"errors.10": "コメントを編集するには遅すぎます。",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "リクエストされたファイルが見つかりません。",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "受信したリクエストを処理できません",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "この操作を実行する権限がありません。",
|
||||
"errors.4": "コメントデータが無効です。",
|
||||
"errors.5": "コメントが見つかりません。ページを再読み込みしてからもう一度お試しください。",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "認証されていません。",
|
||||
"errors.to-many-request": "最大リクエスト回数に達しました。",
|
||||
"errors.unexpected-error": "不明なエラーが発生しました.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "ピン留めしたコメント",
|
||||
"root.powered-by": "提供元: <a>Remark42</a>",
|
||||
"root.show-more": "その他を表示",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "番号付きリストの追加",
|
||||
"toolbar.quote": "引用符の挿入",
|
||||
"toolbar.unordered-list": "記号付きリストの追加",
|
||||
"user-info.last-comments": "{userName}の最新コメント",
|
||||
"user-info.unexpected-error": "不明なエラーが発生しました",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "匿名ユーザーは投稿できません",
|
||||
"vote.deleted": "削除済みコメントには投票できません",
|
||||
"vote.guest": "投票するにはサインインしてください",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "로드 중…",
|
||||
"auth.oauth-button": "{provider}(으)로 로그인",
|
||||
"auth.oauth-source": "소셜 네트워크 사용",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "또는",
|
||||
"auth.signin": "로그인",
|
||||
"auth.signout": "로그아웃하시겠어요",
|
||||
"auth.submit": "확인",
|
||||
"auth.symbols-restriction": "사용자 이름에는 문자, 숫자, 밑줄 또는 공백만 포함되어야 합니다",
|
||||
"auth.user-not-found": "사용자를 찾을 수 없습니다",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "댓글 활성화",
|
||||
"authPanel.enable-cookies": "로그인 및 댓글 작업을 할 수 있도록 쿠키 허용",
|
||||
"authPanel.hide-settings": "설정 숨기기",
|
||||
"authPanel.logged-as": "다음으로 로그인되어 있습니다",
|
||||
"authPanel.logout": "로그아웃하시겠어요?",
|
||||
"authPanel.new-page": "새 페이지",
|
||||
"authPanel.read-only": "읽기 전용",
|
||||
"authPanel.request-to-delete-data": "내 데이터 제거 요청",
|
||||
"authPanel.show-settings": "설정 표시",
|
||||
"blockingDuration.day": "1일",
|
||||
"blockingDuration.month": "1개월",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "오래된 순",
|
||||
"commentsSort.recently-updated": "최신 업데이트 순",
|
||||
"commentsSort.worst": "최저",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "문제가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
"errors.1": "댓글을 찾을 수 없습니다. 페이지를 새로고침하여 다시 시도하세요.",
|
||||
"errors.10": "댓글을 추가하기에 너무 늦었습니다.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "요청한 파일을 찾을 수 없습니다.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "수신 요청 처리가 실패했습니다.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "이 작업을 수행할 수 있는 권한이 없습니다.",
|
||||
"errors.4": "댓글 데이터가 유효하지 않습니다.",
|
||||
"errors.5": "댓글을 찾을 수 없습니다. 페이지를 새로고침하여 다시 시도하세요.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "권한이 없습니다.",
|
||||
"errors.to-many-request": "최대 요청 한도에 도달했습니다.",
|
||||
"errors.unexpected-error": "문제가 발생했습니다.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "고정된 댓글",
|
||||
"root.powered-by": "<a>Remark42</a> 제공",
|
||||
"root.show-more": "더보기",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "숫자 지정된 목록 추가",
|
||||
"toolbar.quote": "인용구 삽입",
|
||||
"toolbar.unordered-list": "글머리 기호가 지정된 목록 추가",
|
||||
"user-info.last-comments": "{userName} 님의 마지막 댓글",
|
||||
"user-info.unexpected-error": "문제가 발생했습니다",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "익명의 사용자는 투표할 수 없습니다",
|
||||
"vote.deleted": "삭제된 댓글에는 투표할 수 없습니다",
|
||||
"vote.guest": "투표하려면 로그인하세요",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Ładuję...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "lub",
|
||||
"auth.signin": "Zaloguj się",
|
||||
"auth.signout": "Wyloguj",
|
||||
"auth.submit": "Potwierdź",
|
||||
"auth.symbols-restriction": "Nazwa użytkownika powinna składać się z liter, numerów, podkreślinków lub spacji",
|
||||
"auth.user-not-found": "Żaden użytkownik nie został znaleziony",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Włącz komentarze",
|
||||
"authPanel.enable-cookies": "Zezwalaj na cookies w celu logowania i komentowania",
|
||||
"authPanel.hide-settings": "Ukryj ustawienia",
|
||||
"authPanel.logged-as": "Jesteś zalogowany jako",
|
||||
"authPanel.logout": "Wyloguj?",
|
||||
"authPanel.new-page": "nowa strona",
|
||||
"authPanel.read-only": "Tylko do odczytu",
|
||||
"authPanel.request-to-delete-data": "Zażądaj usunięcia swoich danych",
|
||||
"authPanel.show-settings": "Pokaż ustawienia",
|
||||
"blockingDuration.day": "Na dzień",
|
||||
"blockingDuration.month": "Na miesiąc",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Najstarsze",
|
||||
"commentsSort.recently-updated": "Ostatnio aktualizowane",
|
||||
"commentsSort.worst": "Najgorsze",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Coś poszło nie tak. Spróbuj ponownie później.",
|
||||
"errors.1": "Komentarz nie został znaleziony. Odśwież stronę i spróbuj ponownie.",
|
||||
"errors.10": "Jest już za późno żeby edytować ten komentarz.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Żądany plik nie został znaleziony.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Nie udało sie sparsować przychodzącego zapytania do struktury danych.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Nie masz wystarczających uprawnień by wykonać te operację.",
|
||||
"errors.4": "Niepoprawne dane komentarza.",
|
||||
"errors.5": "Komentarz nie może zostać odnaleziony. Odśwież stronę i spróbuj ponownie.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Nie jesteś upoważniony.",
|
||||
"errors.to-many-request": "Przekroczyłeś maksymalny limit zapytań.",
|
||||
"errors.unexpected-error": "Coś poszło nie tak.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Przypięte komentarze",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Pokaż więcej",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Dodaj numerowaną listę",
|
||||
"toolbar.quote": "Dodaj cytat",
|
||||
"toolbar.unordered-list": "Dodaj listę punktowaną",
|
||||
"user-info.last-comments": "Ostatnie komentarze {userName}",
|
||||
"user-info.unexpected-error": "Coś poszło nie tak",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Anonimowi użytkownicy nie mają prawa głosu",
|
||||
"vote.deleted": "Nie można głosować na usunięte komentarze",
|
||||
"vote.guest": "Zaloguj się żeby głosować",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Загрузка...",
|
||||
"auth.oauth-button": "Войти через {provider}",
|
||||
"auth.oauth-source": "Использовать социальные сети",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "или",
|
||||
"auth.signin": "Войти",
|
||||
"auth.signout": "Выйти",
|
||||
"auth.submit": "Отправить",
|
||||
"auth.symbols-restriction": "Имя пользователя должно начинаться с буквы и содержать только латинские буквы, цифры, знаки подчеркивания и пробелы",
|
||||
"auth.user-not-found": "Пользователь не найден",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Включить комментарии",
|
||||
"authPanel.enable-cookies": "Разрешите использование файлов cookie, чтобы авторизоваться и комментировать",
|
||||
"authPanel.hide-settings": "Скрыть настройки",
|
||||
"authPanel.logged-as": "Вы вошли как",
|
||||
"authPanel.logout": "Выйти?",
|
||||
"authPanel.new-page": "новой странице",
|
||||
"authPanel.read-only": "Только для чтения",
|
||||
"authPanel.request-to-delete-data": "Запросить удаление своих данных",
|
||||
"authPanel.show-settings": "Показать настройки",
|
||||
"blockingDuration.day": "На день",
|
||||
"blockingDuration.month": "На месяц",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Самые старые",
|
||||
"commentsSort.recently-updated": "Обновленные недавно",
|
||||
"commentsSort.worst": "Худшие",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Что-то пошло не так. Попробуйте еще раз позже.",
|
||||
"errors.1": "Комментарий не найден. Обновите страницу и попробуйте еще раз.",
|
||||
"errors.10": "Время редактирования комментария истекло.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Не удалось найти запрошенный файл.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Не удалось обработать ответ от сервера.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "У вас недостаточно прав для выполнения этого действия.",
|
||||
"errors.4": "Комментарий содержит недопустимые данные.",
|
||||
"errors.5": "Комментарий не найден. Обновите страницу и попробуйте еще раз.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Не авторизован.",
|
||||
"errors.to-many-request": "Достигнуто максимальное число запросов.",
|
||||
"errors.unexpected-error": "Что-то пошло не так.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Закрепленные комментарии",
|
||||
"root.powered-by": "Работает на базе <a>Remark42</a>",
|
||||
"root.show-more": "Показать еще",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Добавить нумерованный список",
|
||||
"toolbar.quote": "Вставить цитату",
|
||||
"toolbar.unordered-list": "Добавить маркированный список",
|
||||
"user-info.last-comments": "Последние комментарии {userName}",
|
||||
"user-info.unexpected-error": "Что-то пошло не так",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Анонимные пользователи не могут голосовать",
|
||||
"vote.deleted": "Нельзя голосовать за удаленный комментарий",
|
||||
"vote.guest": "Войдите, чтобы проголосовать",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Yükleniyor...",
|
||||
"auth.oauth-button": "{provider} ile Oturum Aç",
|
||||
"auth.oauth-source": "Sosyal Ağ Kullan",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "veya",
|
||||
"auth.signin": "Giriş yap",
|
||||
"auth.signout": "Çıkış yap",
|
||||
"auth.submit": "Gönder",
|
||||
"auth.symbols-restriction": "Kullanıcı adı harf ile başlayıp; yalnızca harf, rakam, alt çizgi veya boşluk içerebilir",
|
||||
"auth.user-not-found": "Kullanıcı bulunamadı",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Yorumları etkinleştir",
|
||||
"authPanel.enable-cookies": "Giriş yapmak ve yorum yazmak için çerezleri etkinleştir",
|
||||
"authPanel.hide-settings": "Ayarları gizle",
|
||||
"authPanel.logged-as": "Kullanıcı adınız",
|
||||
"authPanel.logout": "Çıkış yap?",
|
||||
"authPanel.new-page": "yeni sayfa",
|
||||
"authPanel.read-only": "Yazmaya kapalı",
|
||||
"authPanel.request-to-delete-data": "Bilgilerimi sil",
|
||||
"authPanel.show-settings": "Ayarları göster",
|
||||
"blockingDuration.day": "bir günlük",
|
||||
"blockingDuration.month": "bir aylık",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "En eskiler",
|
||||
"commentsSort.recently-updated": "En son güncellenen",
|
||||
"commentsSort.worst": "En kötüler",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
|
||||
"errors.1": "Yorum bulunamadı. Lütfen sayfayı yenileyip tekrar deneyin.",
|
||||
"errors.10": "Yorumu düzenlemek için artık çok geç.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "İstenilen dosya bulunamadı.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Gelen talep işlenemedi.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Bu işlemi yapmak için yetkiniz yok.",
|
||||
"errors.4": "Yorum verisi geçersiz.",
|
||||
"errors.5": "Yorum bulunamadı. Lütfen sayfayı yenileyip tekrar deneyin.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Yetki yetersiz.",
|
||||
"errors.to-many-request": "Maksimum talep limitine ulaştınız.",
|
||||
"errors.unexpected-error": "Bir hata oluştu.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Sabitlenmiş yorumlar",
|
||||
"root.powered-by": "<a>Remark42</a> ile üretildi",
|
||||
"root.show-more": "Daha fazla göster",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Sıralı liste ekle",
|
||||
"toolbar.quote": "Alıntı ekle",
|
||||
"toolbar.unordered-list": "Liste ekle",
|
||||
"user-info.last-comments": "{userName} tarafından son yorumlar",
|
||||
"user-info.unexpected-error": "Bir hata oluştu",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Anonim kullanıcılar oy kullanamaz",
|
||||
"vote.deleted": "Silinmiş yorum oylanamaz",
|
||||
"vote.guest": "Oy vermek için giriş yapın",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "Завантаження...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "або",
|
||||
"auth.signin": "Увійти",
|
||||
"auth.signout": "Вийти",
|
||||
"auth.submit": "Відправити",
|
||||
"auth.symbols-restriction": "Ім’я користувача повинно починатися з літери і містити тільки латинські букви,цифри,знаки підкреслення і прогалини",
|
||||
"auth.user-not-found": "Користувач не знайдений",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Увімкнути коментари",
|
||||
"authPanel.enable-cookies": "Дозвольте Cookies",
|
||||
"authPanel.hide-settings": "Заховати налаштування",
|
||||
"authPanel.logged-as": "Ви увійшли як",
|
||||
"authPanel.logout": "Вийти?",
|
||||
"authPanel.new-page": "нова сторінка",
|
||||
"authPanel.read-only": "Тільки для читання",
|
||||
"authPanel.request-to-delete-data": "Запросити видалення моїх даних",
|
||||
"authPanel.show-settings": "Показати налаштування",
|
||||
"blockingDuration.day": "На день",
|
||||
"blockingDuration.month": "На місяць",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Старі",
|
||||
"commentsSort.recently-updated": "Нещодавно оновлені",
|
||||
"commentsSort.worst": "Гірші",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Щось пішло не так,спробуйте ще раз пізніше.",
|
||||
"errors.1": "Коментар не знайдений. Перезавантажте сторінку і спробуйте ще раз.",
|
||||
"errors.10": "Час редагування коментаря минув.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Запрашиваемый файл не найден.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Не вдалося обробити відповідь від сервера.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Недостатньо прав на здійснення цієї дії.",
|
||||
"errors.4": "Неправильно відформатований коментар.",
|
||||
"errors.5": "Коментар не знайдений. Перезавантажте сторінку і спробуйте ще раз.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Не авторизований.",
|
||||
"errors.to-many-request": "Занадто багато запитів.",
|
||||
"errors.unexpected-error": "Щось пішло не так.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Закріпленні коментарі",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Показати ще",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Упорядкованний список",
|
||||
"toolbar.quote": "Цитата",
|
||||
"toolbar.unordered-list": "Неупорядкованний список",
|
||||
"user-info.last-comments": "Останні коментарі {userName}",
|
||||
"user-info.unexpected-error": "Щось пішло не так",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Не можна голосувати анонімному користувачу",
|
||||
"vote.deleted": "Не можна голосувати за видалений коментар",
|
||||
"vote.guest": "Увійдіть в систему для голосування",
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
"auth.back": "Quay lại",
|
||||
"auth.email-address": "Địa chỉ Email",
|
||||
"auth.loading": "Đang tải...",
|
||||
"auth.oauth-button": "Sign In with {provider}",
|
||||
"auth.oauth-button": "Đăng nhập with {provider}",
|
||||
"auth.oauth-source": "Use Social Network",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "hoặc",
|
||||
"auth.signin": "Sign In",
|
||||
"auth.signin": "Đăng nhập",
|
||||
"auth.signout": "Thoát",
|
||||
"auth.submit": "Gửi đi",
|
||||
"auth.symbols-restriction": "Tên người dùng chỉ được chứa các chữ cái, số, dấu gạch dưới hoặc dấu cách",
|
||||
"auth.user-not-found": "Không tìm thấy người dùng nào",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "Mở bình luận",
|
||||
"authPanel.enable-cookies": "Cho phép cookies đăng nhập và bình luận",
|
||||
"authPanel.hide-settings": "Ẩn cài đặt",
|
||||
"authPanel.logged-as": "Đã đăng nhập bằng",
|
||||
"authPanel.logout": "Thoát?",
|
||||
"authPanel.new-page": "trang mới",
|
||||
"authPanel.read-only": "Chỉ đọc",
|
||||
"authPanel.request-to-delete-data": "Yêu cầu xóa dữ liệu của tôi",
|
||||
"authPanel.show-settings": "Hiện cài đặt",
|
||||
"blockingDuration.day": "Trong một ngày",
|
||||
"blockingDuration.month": "Trong một tháng",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "Cũ nhất",
|
||||
"commentsSort.recently-updated": "Mới được cập nhật",
|
||||
"commentsSort.worst": "Tệ nhất",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "Có gì đó sai sai. Vui lòng thử lại sau.",
|
||||
"errors.1": "Bình luận không được tìm thấy, xin hãy làm mới trang và thử lại.",
|
||||
"errors.10": "Đã quá muộn để sửa bình luận.",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "Không tìm thấy file được yêu cầu.",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "Yêu cầu đến không quản lý được.",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "Bạn không có quyền thực hiện thao tác này.",
|
||||
"errors.4": "Dữ liệu bình luận không hợp lệ.",
|
||||
"errors.5": "Bình luận không được tìm thấy, xin hãy làm mới trang và thử lại.",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "Không được phép.",
|
||||
"errors.to-many-request": "Bạn đã đạt đến giới hạn yêu cầu tối đa.",
|
||||
"errors.unexpected-error": "Có gì đó sai sai.",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "Bình buận đã ghim",
|
||||
"root.powered-by": "Powered by <a>Remark42</a>",
|
||||
"root.show-more": "Xem thêm",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "Thêm danh sách số",
|
||||
"toolbar.quote": "Thêm trích dẫn",
|
||||
"toolbar.unordered-list": "Thêm danh sách",
|
||||
"user-info.last-comments": "Bình luận cuối cùng của {userName}",
|
||||
"user-info.unexpected-error": "Có gì đó sai sai",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "Người dùng ẩn danh không thể vote",
|
||||
"vote.deleted": "Không thể vote bình luận đã xoá",
|
||||
"vote.guest": "Đăng nhập để vote",
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
"auth.loading": "加载中...",
|
||||
"auth.oauth-button": "使用 {provider} 登录",
|
||||
"auth.oauth-source": "使用社交网络",
|
||||
"auth.open-profile": "Open My Profile",
|
||||
"auth.or": "或",
|
||||
"auth.signin": "登录",
|
||||
"auth.signout": "登出",
|
||||
"auth.submit": "提交",
|
||||
"auth.symbols-restriction": "用户名只能包含字母、数字、下划线或空格",
|
||||
"auth.user-not-found": "未找到用户",
|
||||
@@ -15,11 +17,8 @@
|
||||
"authPanel.enable-comments": "启用评论",
|
||||
"authPanel.enable-cookies": "允许使用 Cookie 登录并发表评论",
|
||||
"authPanel.hide-settings": "隐藏设置",
|
||||
"authPanel.logged-as": "您以以下身份登录",
|
||||
"authPanel.logout": "登出?",
|
||||
"authPanel.new-page": "新页面",
|
||||
"authPanel.read-only": "只读",
|
||||
"authPanel.request-to-delete-data": "要求删除我的数据",
|
||||
"authPanel.show-settings": "显示设置",
|
||||
"blockingDuration.day": "一天",
|
||||
"blockingDuration.month": "一个月",
|
||||
@@ -81,6 +80,7 @@
|
||||
"commentsSort.oldest": "最旧",
|
||||
"commentsSort.recently-updated": "最近更新",
|
||||
"commentsSort.worst": "最差",
|
||||
"empty-state": "Don't have comments yet",
|
||||
"errors.0": "出了些问题,请稍后再试。",
|
||||
"errors.1": "找不到评论。请刷新页面,然后重试。",
|
||||
"errors.10": "现在编辑评论已经太晚了。",
|
||||
@@ -94,6 +94,7 @@
|
||||
"errors.18": "找不到请求的文件。",
|
||||
"errors.19": "Comment contains restricted words.",
|
||||
"errors.2": "处理传入请求失败。",
|
||||
"errors.20": "Posted image not found. Please try to upload it again.",
|
||||
"errors.3": "您无权执行此操作。",
|
||||
"errors.4": "评论数据无效。",
|
||||
"errors.5": "找不到评论。请刷新页面,然后重试。",
|
||||
@@ -106,6 +107,8 @@
|
||||
"errors.not-authorized": "未经授权。",
|
||||
"errors.to-many-request": "您已达到最大请求数限制。",
|
||||
"errors.unexpected-error": "出了些问题。",
|
||||
"profile.close": "Close profile",
|
||||
"profile.request-to-delete-data": "Request my data removal",
|
||||
"root.pinned-comments": "固定的评论",
|
||||
"root.powered-by": "由 <a>Remark42</a> 提供支持",
|
||||
"root.show-more": "显示更多",
|
||||
@@ -153,8 +156,8 @@
|
||||
"toolbar.ordered-list": "添加编号列表",
|
||||
"toolbar.quote": "插入引用文本",
|
||||
"toolbar.unordered-list": "添加项目符号列表",
|
||||
"user-info.last-comments": "最近由 {userName} 发表的评论",
|
||||
"user-info.unexpected-error": "出了些问题",
|
||||
"user.my-comments": "My recent comments",
|
||||
"user.recent-comments": "Recent comments",
|
||||
"vote.anonymous": "匿名用户无法投票",
|
||||
"vote.deleted": "无法为已删除的评论投票",
|
||||
"vote.guest": "登录以投票",
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { setAttributes, setStyles, StylesDeclaration } from 'utils/set-dom-props';
|
||||
import { createIframe } from 'utils/create-iframe';
|
||||
import type { Profile } from 'common/types';
|
||||
|
||||
let root: HTMLDivElement | null = null;
|
||||
let iframe: HTMLIFrameElement | null = null;
|
||||
|
||||
function removeIframe() {
|
||||
iframe?.parentNode?.removeChild(iframe);
|
||||
}
|
||||
|
||||
function createElement<K extends keyof HTMLElementTagNameMap>(
|
||||
tagName: K,
|
||||
styles: StylesDeclaration,
|
||||
attrs?: Record<string, string>
|
||||
) {
|
||||
const element = document.createElement(tagName);
|
||||
setStyles(element, styles);
|
||||
setAttributes(element, attrs);
|
||||
return element;
|
||||
}
|
||||
|
||||
function createFragment(params: Profile & Record<string, string | unknown>) {
|
||||
removeIframe();
|
||||
iframe = createIframe({ ...params, page: 'profile', styles: styles.iframe });
|
||||
|
||||
if (!root) {
|
||||
root = createElement('div', styles.root);
|
||||
document.body.appendChild(root);
|
||||
}
|
||||
|
||||
root.appendChild(iframe);
|
||||
setStyles(root, styles.rootShowed);
|
||||
setTimeout(() => iframe?.focus());
|
||||
}
|
||||
|
||||
function animateAppear(): void {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!root || !iframe) {
|
||||
return;
|
||||
}
|
||||
setStyles(root, styles.rootAppear);
|
||||
});
|
||||
}
|
||||
|
||||
function animateDisappear(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
function handleTransitionEnd() {
|
||||
resolve();
|
||||
|
||||
if (!root) {
|
||||
return;
|
||||
}
|
||||
setStyles(root, styles.rootHidden);
|
||||
root.removeEventListener('transitionend', handleTransitionEnd);
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
if (!root || !iframe) {
|
||||
return;
|
||||
}
|
||||
setStyles(root, styles.rootDissapear);
|
||||
root.addEventListener('transitionend', handleTransitionEnd);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeydown(evt: KeyboardEvent) {
|
||||
console.log(evt.code);
|
||||
if (evt.code !== 'Escape') {
|
||||
return;
|
||||
}
|
||||
closeProfile();
|
||||
}
|
||||
|
||||
export function openProfile(params: Profile & Record<string, string | unknown>) {
|
||||
setStyles(document.body, { overflow: 'hidden' });
|
||||
createFragment(params);
|
||||
animateAppear();
|
||||
window.addEventListener('keydown', handleKeydown);
|
||||
}
|
||||
|
||||
export function closeProfile() {
|
||||
window.removeEventListener('keydown', handleKeydown);
|
||||
animateDisappear().then(() => {
|
||||
removeIframe();
|
||||
document.body.style.removeProperty('overflow');
|
||||
});
|
||||
}
|
||||
|
||||
const styles = {
|
||||
root: {
|
||||
display: 'none',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
transition: 'opacity 0.5s ease-in',
|
||||
background: 'rgba(0, 0, 0, .4)',
|
||||
opacity: 0,
|
||||
zIndex: 99999999,
|
||||
},
|
||||
rootShowed: {
|
||||
display: 'block',
|
||||
},
|
||||
rootHidden: {
|
||||
display: 'none',
|
||||
},
|
||||
rootAppear: {
|
||||
opacity: '1',
|
||||
},
|
||||
rootDissapear: {
|
||||
opacity: '0',
|
||||
transition: 'opacity 0.3s ease-out',
|
||||
},
|
||||
iframe: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
};
|
||||
+23
-4
@@ -5,16 +5,17 @@ import { IntlProvider } from 'react-intl';
|
||||
|
||||
import { loadLocale } from 'utils/loadLocale';
|
||||
import { getLocale } from 'utils/getLocale';
|
||||
import { parseQuery } from 'utils/parse-query';
|
||||
import { parseMessage } from 'utils/post-message';
|
||||
import { parseBooleansFromDictionary } from 'utils/parse-booleans-from-dictionary';
|
||||
import { ConnectedRoot } from 'components/root';
|
||||
import { UserInfo } from 'components/user-info';
|
||||
import { Profile } from 'components/profile';
|
||||
import { store } from 'store';
|
||||
import { NODE_ID, BASE_URL } from 'common/constants';
|
||||
import { StaticStore } from 'common/static-store';
|
||||
import { getConfig } from 'common/api';
|
||||
import { fetchHiddenUsers } from 'store/user/actions';
|
||||
import { restoreCollapsedThreads } from 'store/thread/actions';
|
||||
import { parseQuery } from 'utils/parseQuery';
|
||||
import { parseBooleansFromDictionary } from 'utils/parse-booleans-from-dictionary';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -36,6 +37,24 @@ async function init(): Promise<void> {
|
||||
const messages = await loadLocale(locale).catch(() => ({}));
|
||||
const boundActions = bindActionCreators({ fetchHiddenUsers, restoreCollapsedThreads }, store.dispatch);
|
||||
|
||||
node.innerHTML = '';
|
||||
|
||||
window.addEventListener('message', (evt) => {
|
||||
const data = parseMessage(evt);
|
||||
|
||||
if (data.theme === 'light') {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
|
||||
if (data.theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
});
|
||||
|
||||
if (params.theme === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
}
|
||||
|
||||
boundActions.fetchHiddenUsers();
|
||||
boundActions.restoreCollapsedThreads();
|
||||
|
||||
@@ -45,7 +64,7 @@ async function init(): Promise<void> {
|
||||
|
||||
render(
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
<Provider store={store}>{params.page === 'user-info' ? <UserInfo /> : <ConnectedRoot />}</Provider>
|
||||
<Provider store={store}>{params.page === 'profile' ? <Profile /> : <ConnectedRoot />}</Provider>
|
||||
</IntlProvider>,
|
||||
node
|
||||
);
|
||||
|
||||
@@ -3,13 +3,6 @@ import { POST_INFO_ACTIONS } from './post-info/types';
|
||||
import { THEME_ACTIONS } from './theme/types';
|
||||
import { THREAD_ACTIONS } from './thread/types';
|
||||
import { USER_ACTIONS } from './user/types';
|
||||
import { USER_INFO_ACTIONS } from './user-info/types';
|
||||
|
||||
/** Merged store actions */
|
||||
export type ACTIONS =
|
||||
| COMMENTS_ACTIONS
|
||||
| POST_INFO_ACTIONS
|
||||
| THEME_ACTIONS
|
||||
| THREAD_ACTIONS
|
||||
| USER_ACTIONS
|
||||
| USER_INFO_ACTIONS;
|
||||
export type ACTIONS = COMMENTS_ACTIONS | POST_INFO_ACTIONS | THEME_ACTIONS | THREAD_ACTIONS | USER_ACTIONS;
|
||||
|
||||
@@ -2,7 +2,6 @@ import * as comments from './comments/reducers';
|
||||
import * as postInfo from './post-info/reducers';
|
||||
import * as theme from './theme/reducers';
|
||||
import * as user from './user/reducers';
|
||||
import * as userInfo from './user-info/reducers';
|
||||
import * as thread from './thread/reducers';
|
||||
|
||||
/** Merged store reducers */
|
||||
@@ -10,7 +9,6 @@ export const rootProvider = {
|
||||
...comments,
|
||||
...theme,
|
||||
...postInfo,
|
||||
...userInfo,
|
||||
...thread,
|
||||
...user,
|
||||
};
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { getUserComments } from 'common/api';
|
||||
import { Comment } from 'common/types';
|
||||
|
||||
import { StoreAction } from '../index';
|
||||
import { USER_INFO_SET } from './types';
|
||||
|
||||
export const fetchInfo = (id: string): StoreAction<Promise<Comment[] | null>> => async (dispatch) => {
|
||||
// TODO: limit
|
||||
const info = await getUserComments(id, 10);
|
||||
|
||||
dispatch({
|
||||
type: USER_INFO_SET,
|
||||
id,
|
||||
comments: info.comments,
|
||||
});
|
||||
return info.comments;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { Comment } from 'common/types';
|
||||
|
||||
import { USER_INFO_SET, USER_INFO_ACTIONS } from './types';
|
||||
|
||||
export interface UserCommentsState {
|
||||
[key: string]: Comment[];
|
||||
}
|
||||
|
||||
export function userComments(state: UserCommentsState = {}, action: USER_INFO_ACTIONS): UserCommentsState {
|
||||
switch (action.type) {
|
||||
case USER_INFO_SET: {
|
||||
return {
|
||||
...state,
|
||||
[action.id]: action.comments,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Comment, User } from 'common/types';
|
||||
|
||||
export const USER_INFO_SET = 'USER-INFO/SET';
|
||||
|
||||
export interface USER_INFO_SET_ACTION {
|
||||
type: typeof USER_INFO_SET;
|
||||
id: User['id'];
|
||||
comments: Comment[];
|
||||
}
|
||||
|
||||
export type USER_INFO_ACTIONS = USER_INFO_SET_ACTION;
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as api from 'common/api';
|
||||
import { logout } from 'components/auth/auth.api';
|
||||
import { User, BlockedUser, BlockTTL } from 'common/types';
|
||||
import { ttlToTime } from 'utils/ttl-to-time';
|
||||
import { getHiddenUsers } from 'utils/get-hidden-users';
|
||||
import { LS_HIDDEN_USERS_KEY } from 'common/constants';
|
||||
import { LS_EMAIL_KEY, LS_HIDDEN_USERS_KEY } from 'common/constants';
|
||||
import { setItem } from 'common/local-storage';
|
||||
|
||||
import { StoreAction } from '../index';
|
||||
@@ -17,7 +18,7 @@ import {
|
||||
USER_SUBSCRIPTION_SET,
|
||||
USER_SET_ACTION,
|
||||
} from './types';
|
||||
import { fetchComments } from '../comments/actions';
|
||||
import { fetchComments, unsetCommentMode } from '../comments/actions';
|
||||
import { COMMENTS_PATCH } from '../comments/types';
|
||||
|
||||
export function setUser(user: User | null = null): USER_SET_ACTION {
|
||||
@@ -27,6 +28,18 @@ export function setUser(user: User | null = null): USER_SET_ACTION {
|
||||
};
|
||||
}
|
||||
|
||||
export function signout(cleanSession = true): StoreAction<Promise<void>> {
|
||||
return async (dispatch) => {
|
||||
if (cleanSession) {
|
||||
await logout();
|
||||
}
|
||||
localStorage.removeItem(LS_EMAIL_KEY);
|
||||
dispatch(setUser());
|
||||
dispatch(unsetCommentMode());
|
||||
dispatch(fetchComments());
|
||||
};
|
||||
}
|
||||
|
||||
export const fetchUser = (): StoreAction<Promise<User | null>> => async (dispatch) => {
|
||||
const user = await api.getUser();
|
||||
dispatch(setUser(user));
|
||||
|
||||
@@ -61,7 +61,9 @@
|
||||
/* Named variables */
|
||||
--primary-color: 0, 170, 170;
|
||||
--primary-brighter-color: 0, 153, 153;
|
||||
--primary-text-color: 38, 38, 38;
|
||||
--secondary-text-color: 100, 116, 139;
|
||||
--primary-background-color: 255, 255, 255;
|
||||
--black-color: 0, 0, 0;
|
||||
--white-color: 255, 255, 255;
|
||||
--error-color: #b91c1c;
|
||||
@@ -74,7 +76,11 @@
|
||||
--line-color: var(--color36);
|
||||
--primary-color: 0, 153, 153;
|
||||
--primary-brighter-color: 0, 170, 170;
|
||||
--primary-text-color: 241, 245, 249;
|
||||
--primary-background-color: 38, 38, 38;
|
||||
--secondary-text-color: 209, 213, 219;
|
||||
--error-color: #ffa0a0;
|
||||
--line-brighter-color: var(--color11);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,33 @@ body {
|
||||
margin: 0;
|
||||
padding: 6px;
|
||||
font-family: PT Sans, Helvetica, Arial, sans-serif;
|
||||
color: rgb(var(--primary-text-color));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
background: unset;
|
||||
font-size: inherit;
|
||||
transition-property: color, background, border-color;
|
||||
transition-timing-function: linear;
|
||||
transition-duration: 150ms;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
#remark42 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Preloader */
|
||||
|
||||
.preloader,
|
||||
.preloader::before,
|
||||
.preloader::after {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { h, ComponentChild } from 'preact';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import { render as originalRender } from '@testing-library/preact';
|
||||
|
||||
import en from 'locales/en.json';
|
||||
|
||||
export function render(children: ComponentChild) {
|
||||
return originalRender(
|
||||
<IntlProvider locale="en" messages={en}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BASE_URL } from 'common/constants.config';
|
||||
import { setStyles, setAttributes, StylesDeclaration } from 'utils/set-dom-props';
|
||||
|
||||
type Params = { [key: string]: unknown; __colors__?: Record<string, string>; styles?: StylesDeclaration };
|
||||
|
||||
export function createIframe({ __colors__, styles, ...params }: Params) {
|
||||
const iframe = document.createElement('iframe');
|
||||
const query = new URLSearchParams(params as Record<string, string>).toString();
|
||||
|
||||
setAttributes(iframe, {
|
||||
src: `${BASE_URL}/web/iframe.html?${query}`,
|
||||
name: JSON.stringify({ __colors__ }),
|
||||
frameborder: '0',
|
||||
allowtransparency: 'true',
|
||||
scrolling: 'no',
|
||||
tabindex: '0',
|
||||
title: 'Comments | Remark42',
|
||||
horizontalscrolling: 'no',
|
||||
verticalscrolling: 'no',
|
||||
});
|
||||
setStyles(iframe, {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
overflow: 'hidden',
|
||||
...styles,
|
||||
});
|
||||
|
||||
return iframe;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseQuery } from './parseQuery';
|
||||
import { parseQuery } from './parse-query';
|
||||
|
||||
describe('parseQuery', () => {
|
||||
it('should return empty object', () => {
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { Theme, UserInfo } from 'common/types';
|
||||
import type { Theme, Profile } from 'common/types';
|
||||
|
||||
type ParentMessage = {
|
||||
inited?: true;
|
||||
scrollTo?: number;
|
||||
height?: number;
|
||||
profile?: UserInfo | null;
|
||||
signout?: true;
|
||||
profile?: Profile | null;
|
||||
};
|
||||
|
||||
type ChildMessage = {
|
||||
@@ -12,6 +13,7 @@ type ChildMessage = {
|
||||
hash?: string;
|
||||
title?: string;
|
||||
theme?: Theme;
|
||||
signout?: true;
|
||||
};
|
||||
|
||||
type AllMessages = ChildMessage & ParentMessage;
|
||||
@@ -0,0 +1,23 @@
|
||||
export type StylesDeclaration = Partial<
|
||||
Record<
|
||||
keyof Omit<
|
||||
CSSStyleDeclaration,
|
||||
'length' | 'parentRule' | 'setProperty' | 'removeProperty' | 'item' | 'getPropertyValue' | 'getPropertyPriority'
|
||||
>,
|
||||
string | number
|
||||
>
|
||||
>;
|
||||
|
||||
export function setStyles(element: HTMLElement, styles: StylesDeclaration = {}) {
|
||||
const entr = Object.entries(styles);
|
||||
|
||||
entr.forEach(([p, v]) => {
|
||||
element.style[p as keyof StylesDeclaration] = `${v}`;
|
||||
});
|
||||
}
|
||||
|
||||
export function setAttributes(element: HTMLElement, attrs: Record<string, string | number> = {}) {
|
||||
Object.entries(attrs).forEach(([p, v]) => {
|
||||
element.setAttribute(p, `${v}`);
|
||||
});
|
||||
}
|
||||
@@ -20,7 +20,6 @@ module.exports = {
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/app/__mocks__/fetch.ts',
|
||||
'<rootDir>/app/__mocks__/localstorage.ts',
|
||||
'<rootDir>/app/__stubs__/react-intl.ts',
|
||||
'<rootDir>/app/__stubs__/remark-config.ts',
|
||||
'<rootDir>/app/__stubs__/static-config.ts',
|
||||
'<rootDir>/app/__stubs__/settings.ts',
|
||||
@@ -31,6 +30,7 @@ module.exports = {
|
||||
'!**/__stubs__/**',
|
||||
'!app/locales/**',
|
||||
'!app/utils/loadLocale.ts',
|
||||
'!app/tests',
|
||||
],
|
||||
globals: {
|
||||
'ts-jest': {
|
||||
|
||||
@@ -33,82 +33,6 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<!-- TODO: we have to move styles in css file -->
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
@keyframes bouncing {
|
||||
0%,
|
||||
80%,
|
||||
100% {
|
||||
box-shadow: 0 10px 0 -10px;
|
||||
}
|
||||
|
||||
40% {
|
||||
box-shadow: 0 10px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.preloader,
|
||||
.preloader::before,
|
||||
.preloader::after {
|
||||
border-radius: 50%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
text-align: left;
|
||||
animation-name: bouncing;
|
||||
animation-timing-function: ease-in-out;
|
||||
animation-fill-mode: both;
|
||||
animation-duration: 1.8s;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
.preloader {
|
||||
/* stylelint-disable-next-line */
|
||||
color: #fff;
|
||||
color: var(--color6, #fff);
|
||||
position: relative;
|
||||
transform: translate3d(0, -10px, 0);
|
||||
animation-delay: -0.16s;
|
||||
}
|
||||
|
||||
.preloader::before,
|
||||
.preloader::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.preloader::before {
|
||||
transform: translate3d(-15px, 0, 0);
|
||||
animation-delay: -0.32s;
|
||||
}
|
||||
|
||||
.preloader::after {
|
||||
transform: translate3d(15px, 0, 0);
|
||||
}
|
||||
|
||||
.preloader_view_iframe {
|
||||
margin: 0 auto;
|
||||
/* stylelint-disable-next-line */
|
||||
color: #888;
|
||||
color: var(--color13, #888);
|
||||
}
|
||||
|
||||
:focus:not(.focus-visible):not(.button) {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
<% if (htmlWebpackPlugin.options.env === 'production') { %>
|
||||
<link rel="stylesheet" href="remark.css" />
|
||||
<% } %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"incremental": true,
|
||||
"target": "es6",
|
||||
"module": "esnext",
|
||||
"lib": ["es2018", "dom"],
|
||||
"lib": ["es2019", "dom"],
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
@@ -12,7 +12,7 @@
|
||||
"allowJs": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"baseUrl": "app" ,
|
||||
"baseUrl": "app",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true
|
||||
|
||||
@@ -19,7 +19,7 @@ const PORT = process.env.PORT || 9000;
|
||||
const REMARK_API_BASE_URL = process.env.REMARK_API_BASE_URL || 'http://127.0.0.1:8080';
|
||||
const DEVSERVER_BASE_PATH = process.env.DEVSERVER_BASE_PATH || 'http://127.0.0.1:9000';
|
||||
const PUBLIC_FOLDER_PATH = path.resolve(__dirname, 'public');
|
||||
const CUSTOM_PROPERTIES_PATH = path.resolve(__dirname, './app/custom-properties.css');
|
||||
const CUSTOM_PROPERTIES_PATH = path.resolve(__dirname, './app/styles/custom-properties.css');
|
||||
|
||||
const genId = incstr.idGenerator();
|
||||
const modulesMap = {};
|
||||
@@ -138,8 +138,16 @@ module.exports = (_, { mode, analyze }) => {
|
||||
sourceMap: isDev,
|
||||
postcssOptions: {
|
||||
plugins: [
|
||||
['postcss-preset-env', { stage: 0 }],
|
||||
['postcss-custom-properties', { importFrom: CUSTOM_PROPERTIES_PATH }],
|
||||
[
|
||||
'postcss-preset-env',
|
||||
{
|
||||
browsers: 'defaults, not IE 11, not samsung 12',
|
||||
stage: 0,
|
||||
features: {
|
||||
'custom-properties': CUSTOM_PROPERTIES_PATH,
|
||||
},
|
||||
},
|
||||
],
|
||||
'cssnano',
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user