Merge branch 'master' into user-info-redux
This commit is contained in:
+1
-1
@@ -43,7 +43,7 @@ module.exports = {
|
||||
'no-undef-init': 2,
|
||||
'no-shadow-restricted-names': 2,
|
||||
'handle-callback-err': 0,
|
||||
'no-lonely-if': 2,
|
||||
'no-lonely-if': 0,
|
||||
'constructor-super': 2,
|
||||
'no-this-before-super': 2,
|
||||
'no-dupe-class-members': 2,
|
||||
|
||||
@@ -69,6 +69,18 @@ export const getUser = () =>
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
/* GDPR */
|
||||
|
||||
export const deleteMe = () =>
|
||||
fetcher.post({
|
||||
url: `/deleteme?site=${siteId}`,
|
||||
});
|
||||
|
||||
export const approveDeleteMe = token =>
|
||||
fetcher.get({
|
||||
url: `/admin/deleteme?token=${token}`,
|
||||
});
|
||||
|
||||
/* admin */
|
||||
export const pinComment = ({ id, url }) =>
|
||||
fetcher.put({
|
||||
@@ -118,6 +130,18 @@ export const getBlocked = () =>
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const disableComments = () =>
|
||||
fetcher.put({
|
||||
url: `/admin/readonly?site=${siteId}&url=${url}&ro=1`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export const enableComments = () =>
|
||||
fetcher.put({
|
||||
url: `/admin/readonly?site=${siteId}&url=${url}&ro=0`,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
export default {
|
||||
logOut,
|
||||
getConfig,
|
||||
@@ -140,4 +164,6 @@ export default {
|
||||
blockUser,
|
||||
unblockUser,
|
||||
getBlocked,
|
||||
disableComments,
|
||||
enableComments,
|
||||
};
|
||||
|
||||
+14
-31
@@ -1,24 +1,24 @@
|
||||
const BASE_URL = 'https://remark42.radio-t.com';
|
||||
const API_BASE = '/api/v1';
|
||||
const NODE_ID = 'remark42';
|
||||
const COUNTER_NODE_CLASSNAME = 'remark42__counter';
|
||||
const COMMENT_NODE_CLASSNAME_PREFIX = 'remark42__comment-';
|
||||
const LAST_COMMENTS_NODE_CLASSNAME = 'remark42__last-comments';
|
||||
const DEFAULT_LAST_COMMENTS_MAX = 15;
|
||||
const DEFAULT_MAX_COMMENT_SIZE = 1000;
|
||||
const MAX_SHOWN_ROOT_COMMENTS = 10;
|
||||
const DEFAULT_SORT = '-score';
|
||||
const PROVIDER_NAMES = {
|
||||
export const BASE_URL = process.env.REMARK_URL;
|
||||
export const API_BASE = '/api/v1';
|
||||
export const NODE_ID = process.env.REMARK_NODE;
|
||||
export const COUNTER_NODE_CLASSNAME = 'remark42__counter';
|
||||
export const COMMENT_NODE_CLASSNAME_PREFIX = 'remark42__comment-';
|
||||
export const LAST_COMMENTS_NODE_CLASSNAME = 'remark42__last-comments';
|
||||
export const DEFAULT_LAST_COMMENTS_MAX = 15;
|
||||
export const DEFAULT_MAX_COMMENT_SIZE = 1000;
|
||||
export const MAX_SHOWN_ROOT_COMMENTS = 10;
|
||||
export const DEFAULT_SORT = '-score';
|
||||
export const PROVIDER_NAMES = {
|
||||
google: 'Google',
|
||||
facebook: 'Facebook',
|
||||
github: 'GitHub',
|
||||
yandex: 'Yandex',
|
||||
dev: 'Dev',
|
||||
};
|
||||
const LS_COLLAPSE_KEY = '__remarkCollapsed';
|
||||
const LS_SORT_KEY = '__remarkSort';
|
||||
export const LS_COLLAPSE_KEY = '__remarkCollapsed';
|
||||
export const LS_SORT_KEY = '__remarkSort';
|
||||
|
||||
const BLOCKING_DURATIONS = [
|
||||
export const BLOCKING_DURATIONS = [
|
||||
{
|
||||
label: 'Permanently',
|
||||
value: 'permanently',
|
||||
@@ -36,20 +36,3 @@ const BLOCKING_DURATIONS = [
|
||||
value: `${60 * 24}m`,
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
BASE_URL,
|
||||
API_BASE,
|
||||
NODE_ID,
|
||||
COUNTER_NODE_CLASSNAME,
|
||||
LAST_COMMENTS_NODE_CLASSNAME,
|
||||
COMMENT_NODE_CLASSNAME_PREFIX,
|
||||
DEFAULT_LAST_COMMENTS_MAX,
|
||||
DEFAULT_MAX_COMMENT_SIZE,
|
||||
MAX_SHOWN_ROOT_COMMENTS,
|
||||
PROVIDER_NAMES,
|
||||
DEFAULT_SORT,
|
||||
LS_COLLAPSE_KEY,
|
||||
LS_SORT_KEY,
|
||||
BLOCKING_DURATIONS,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import axios from 'axios';
|
||||
|
||||
import { BASE_URL, API_BASE } from './constants';
|
||||
import { siteId } from './settings';
|
||||
import store from './store';
|
||||
|
||||
const fetcher = {};
|
||||
const methods = ['get', 'post', 'put', 'patch', 'delete', 'head'];
|
||||
@@ -30,12 +31,19 @@ methods.forEach(method => {
|
||||
|
||||
parameters.url = `${basename}${url}`;
|
||||
|
||||
if (method !== 'post' && !parameters.url.includes('?site=') && !parameters.url.includes('&site=')) {
|
||||
if (siteId && method !== 'post' && !parameters.url.includes('?site=') && !parameters.url.includes('&site=')) {
|
||||
parameters.url += (parameters.url.includes('?') ? '&' : '?') + `site=${siteId}`;
|
||||
}
|
||||
|
||||
axios(parameters)
|
||||
.then(res => resolve(res.data))
|
||||
.then(res => {
|
||||
const date = ('date' in res.headers && res.headers.date) || '';
|
||||
const timestamp = isNaN(Date.parse(date)) ? 0 : Date.parse(date);
|
||||
const timeDiff = (new Date() - timestamp) / 1000;
|
||||
store.set('serverClientTimeDiff', timeDiff);
|
||||
|
||||
resolve(res.data);
|
||||
})
|
||||
.catch(error => reject(error));
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'core-js/es7/promise';
|
||||
import 'focus-visible';
|
||||
|
||||
export default function loadPolyfills() {
|
||||
const fillCoreJs = () => {
|
||||
|
||||
@@ -11,3 +11,4 @@ const querySettings =
|
||||
export const siteId = querySettings['site_id'];
|
||||
export const url = querySettings['url'];
|
||||
export const maxShownComments = querySettings['max_shown_comments'];
|
||||
export const token = querySettings['token'];
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
.auth-panel__column {
|
||||
&:nth-child(1) {
|
||||
overflow: hidden;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.auth-panel__sign-out {
|
||||
margin-left: 5px;
|
||||
}
|
||||
@@ -48,7 +48,7 @@ describe('<AuthPanel />', () => {
|
||||
|
||||
const userInfo = authPanelColumn[0];
|
||||
|
||||
expect(userInfo.textContent).toEqual(expect.stringContaining('You signed in as John. Sign out?'));
|
||||
expect(userInfo.textContent).toEqual(expect.stringContaining('You signed in as John'));
|
||||
});
|
||||
});
|
||||
describe('For admin user', () => {
|
||||
@@ -67,7 +67,7 @@ describe('<AuthPanel />', () => {
|
||||
|
||||
const adminAction = container.querySelector('.auth-panel__admin-action');
|
||||
|
||||
expect(adminAction.textContent).toEqual('Show blocked');
|
||||
expect(adminAction.textContent).toEqual('Show blocked users');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
|
||||
export default ({ id }) => (
|
||||
<div className="auth-panel__user-id" title={id}>
|
||||
{id}
|
||||
</div>
|
||||
);
|
||||
@@ -1,3 +1,5 @@
|
||||
.auth-panel__user-id {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.auth-panel__username {
|
||||
cursor: default;
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
/** @jsx h */
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
import UserId from './__user-id/auth-panel__user-id';
|
||||
import Dropdown, { DropdownItem } from 'components/dropdown';
|
||||
import Button from 'components/button';
|
||||
import { PROVIDER_NAMES } from 'common/constants';
|
||||
import { requestDeletion } from 'utils/email';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
|
||||
export default class AuthPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.toggleUserId = this.toggleUserId.bind(this);
|
||||
this.toggleBlockedVisibility = this.toggleBlockedVisibility.bind(this);
|
||||
this.toggleCommentsAvailability = this.toggleCommentsAvailability.bind(this);
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
}
|
||||
|
||||
@@ -19,10 +23,6 @@ export default class AuthPanel extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
toggleUserId() {
|
||||
this.setState({ isUserIdVisible: !this.state.isUserIdVisible });
|
||||
}
|
||||
|
||||
toggleBlockedVisibility() {
|
||||
if (!this.state.isBlockedVisible) {
|
||||
if (this.props.onBlockedUsersShow) this.props.onBlockedUsersShow();
|
||||
@@ -31,8 +31,25 @@ export default class AuthPanel extends Component {
|
||||
this.setState({ isBlockedVisible: !this.state.isBlockedVisible });
|
||||
}
|
||||
|
||||
render(props, { isUserIdVisible, isBlockedVisible }) {
|
||||
const { user, providers = [], sort } = props;
|
||||
toggleCommentsAvailability() {
|
||||
if (this.props.isCommentsDisabled) {
|
||||
if (this.props.onCommentsEnable) {
|
||||
this.props.onCommentsEnable();
|
||||
}
|
||||
} else {
|
||||
if (this.props.onCommentsDisable) {
|
||||
this.props.onCommentsDisable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getUserTitle() {
|
||||
const { user } = this.props;
|
||||
return <span className="auth-panel__username">{user.name}</span>;
|
||||
}
|
||||
|
||||
render(props, { isBlockedVisible }) {
|
||||
const { user, providers = [], sort, isCommentsDisabled } = props;
|
||||
|
||||
const sortArray = getSortArray(sort);
|
||||
|
||||
@@ -42,13 +59,20 @@ export default class AuthPanel extends Component {
|
||||
{loggedIn && (
|
||||
<div className="auth-panel__column">
|
||||
You signed in as{' '}
|
||||
<strong {...getHandleClickProps(this.toggleUserId)} className="auth-panel__username">
|
||||
{user.name}
|
||||
</strong>
|
||||
{isUserIdVisible && <span className="auth-panel__user-id"> ({user.id})</span>}.{' '}
|
||||
<span {...getHandleClickProps(props.onSignOut)} className="auth-panel__pseudo-link" role="link">
|
||||
<Dropdown title={user.name}>
|
||||
<DropdownItem separator>
|
||||
<UserId id={user.id} />
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem>
|
||||
<Button mods={{ kind: 'link' }} onClick={() => requestDeletion().then(props.onSignOut)}>
|
||||
Request my data removal
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</Dropdown>{' '}
|
||||
<Button className="auth-panel__sign-out" mods={{ kind: 'link' }} onClick={props.onSignOut}>
|
||||
Sign out?
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -82,7 +106,19 @@ export default class AuthPanel extends Component {
|
||||
{...getHandleClickProps(this.toggleBlockedVisibility)}
|
||||
role="link"
|
||||
>
|
||||
{isBlockedVisible ? 'Hide' : 'Show'} blocked
|
||||
{isBlockedVisible ? 'Hide' : 'Show'} blocked users
|
||||
</span>
|
||||
)}
|
||||
|
||||
{user.admin && ' • '}
|
||||
|
||||
{user.admin && (
|
||||
<span
|
||||
className="auth-panel__pseudo-link auth-panel__admin-action"
|
||||
{...getHandleClickProps(this.toggleCommentsAvailability)}
|
||||
role="link"
|
||||
>
|
||||
{isCommentsDisabled ? 'Enable' : 'Disable'} comments
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@ require('./__pseudo-link/auth-panel__pseudo-link.scss');
|
||||
require('./__select/auth-panel__select.scss');
|
||||
require('./__select-label/auth-panel__select-label.scss');
|
||||
require('./__sort/auth-panel__sort.scss');
|
||||
require('./__username/auth-panel__username.scss');
|
||||
|
||||
require('./__user-id/auth-panel__user-id.scss');
|
||||
require('./__sign-out/auth-panel__sign-out.scss');
|
||||
|
||||
require('./_logged-in/auth-panel_logged-in.scss');
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.button_focused:not(.button_clicked) {
|
||||
outline-width: 5px;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.button_kind_text {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button_kind_link {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
color: #0aa;
|
||||
|
||||
&:hover {
|
||||
color: #06c5c5;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/** @jsx h */
|
||||
import { Component, h } from 'preact';
|
||||
import noop from '../../utils/noop';
|
||||
|
||||
export default class Button extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isClicked: false,
|
||||
isFocused: false,
|
||||
};
|
||||
|
||||
this.onMouseDown = this.onMouseDown.bind(this);
|
||||
this.onFocus = this.onFocus.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
}
|
||||
|
||||
onMouseDown() {
|
||||
this.setState({
|
||||
isClicked: true,
|
||||
});
|
||||
}
|
||||
|
||||
onClick(e) {
|
||||
this.props.onClick(e);
|
||||
}
|
||||
|
||||
onBlur(e) {
|
||||
this.setState({
|
||||
isClicked: false,
|
||||
isFocused: false,
|
||||
});
|
||||
|
||||
this.props.onBlur(e);
|
||||
}
|
||||
|
||||
onFocus(e) {
|
||||
this.setState({
|
||||
isFocused: true,
|
||||
});
|
||||
|
||||
this.props.onFocus(e);
|
||||
}
|
||||
|
||||
render(props, state) {
|
||||
const { children, mix, mods, ...rest } = props;
|
||||
const { isClicked, isFocused } = state;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
className={b('button', { mix, mods }, { clicked: isClicked, focused: isFocused })}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Button.defaultProps = {
|
||||
type: 'button',
|
||||
onClick: noop,
|
||||
onBlur: noop,
|
||||
onFocus: noop,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
.button {
|
||||
cursor: pointer;
|
||||
outline-width: 0;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default } from './button';
|
||||
|
||||
require('./button.scss');
|
||||
require('./_kind/button_kind.scss');
|
||||
require('./_focused/button_focused.scss');
|
||||
@@ -9,10 +9,6 @@
|
||||
color: #31c7c5;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
+ .comment__action {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -10,8 +10,4 @@
|
||||
&:hover {
|
||||
color: #0aa;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
user-select: none;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
opacity: 0;
|
||||
|
||||
@media (hover: hover) {
|
||||
font-weight: 700;
|
||||
opacity: 0;
|
||||
&:hover,
|
||||
&:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
background: url('comment__vote.svg') center no-repeat;
|
||||
background-size: contain;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
background-image: url('_selected/comment__vote_selected.svg');
|
||||
|
||||
@@ -83,7 +83,8 @@ export default class Comment extends Component {
|
||||
|
||||
if (userId === commentUserId) {
|
||||
const editDuration = store.get('config') && store.get('config').edit_duration;
|
||||
const getEditTimeLeft = () => Math.floor(editDuration - (new Date() - new Date(data.time)) / 1000);
|
||||
const timeDiff = store.get('serverClientTimeDiff') || 0;
|
||||
const getEditTimeLeft = () => Math.floor(editDuration - ((new Date() - new Date(data.time)) / 1000 - timeDiff));
|
||||
|
||||
if (getEditTimeLeft() > 0) {
|
||||
this.editTimerInterval = setInterval(() => {
|
||||
@@ -320,7 +321,7 @@ export default class Comment extends Component {
|
||||
editTimeLeft,
|
||||
}
|
||||
) {
|
||||
const { data, mods = {} } = props;
|
||||
const { data, mods = {}, isCommentsDisabled } = props;
|
||||
const isAdmin = !guest && store.get('user').admin;
|
||||
const isGuest = guest || !Object.keys(store.get('user')).length;
|
||||
const isCurrentUser = (data.user && data.user.id) === (store.get('user') && store.get('user').id);
|
||||
@@ -500,6 +501,7 @@ export default class Comment extends Component {
|
||||
|
||||
<div className="comment__actions">
|
||||
{!deleted &&
|
||||
!isCommentsDisabled &&
|
||||
!mods.disabled &&
|
||||
!isGuest &&
|
||||
mods.view !== 'user' && (
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
.dropdown__content {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
outline-width: 0;
|
||||
display: none;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 5px);
|
||||
width: 170px;
|
||||
background-color: #fff;
|
||||
border: 2px solid #259c9a;
|
||||
border-radius: 3px;
|
||||
padding: 0 0 5px;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @jsx h */
|
||||
import { h } from 'preact';
|
||||
|
||||
export default function DropdownItem(props) {
|
||||
const { children, separator = false, mix, mods } = props;
|
||||
|
||||
return <div className={b('dropdown__item', { mix, mods }, { separator })}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
.dropdown__item {
|
||||
a, button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
|
||||
&_separator {
|
||||
border-bottom: 1px solid #259c9a;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.dropdown__items {
|
||||
padding: 5px 0;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.dropdown__title {
|
||||
border: none;
|
||||
background: none;
|
||||
font-weight: bold;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&::after {
|
||||
content: '\25BE';
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.dropdown_active {
|
||||
.dropdown__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/** @jsx h */
|
||||
import { Component, h } from 'preact';
|
||||
|
||||
import Button from 'components/button';
|
||||
|
||||
export default class Dropdown extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isActive: props.isActive || false,
|
||||
};
|
||||
|
||||
this.onTitleClick = this.onTitleClick.bind(this);
|
||||
this.onOutsideClick = this.onOutsideClick.bind(this);
|
||||
this.receiveMessage = this.receiveMessage.bind(this);
|
||||
}
|
||||
|
||||
onTitleClick() {
|
||||
this.setState({
|
||||
isActive: !this.state.isActive,
|
||||
});
|
||||
|
||||
if (this.props.onTitleClick) {
|
||||
this.props.onTitleClick();
|
||||
}
|
||||
}
|
||||
|
||||
receiveMessage(e) {
|
||||
try {
|
||||
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
|
||||
|
||||
if (data.clickOutside) {
|
||||
if (this.state.isActive) {
|
||||
this.setState({
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
onOutsideClick(e) {
|
||||
if (!this.rootNode.contains(e.target)) {
|
||||
if (this.state.isActive) {
|
||||
this.setState({
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('click', this.onOutsideClick);
|
||||
|
||||
window.addEventListener('message', this.receiveMessage);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('click', this.onOutsideClick);
|
||||
|
||||
window.removeEventListener('message', this.receiveMessage);
|
||||
}
|
||||
|
||||
render(props, { isActive }) {
|
||||
const { title, heading, children, mix, mods } = props;
|
||||
|
||||
return (
|
||||
<div className={b('dropdown', { mix, mods }, { active: isActive })} ref={r => (this.rootNode = r)}>
|
||||
<Button
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isActive && 'true'}
|
||||
mix="dropdown__title"
|
||||
type="button"
|
||||
onClick={this.onTitleClick}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
|
||||
<div className="dropdown__content" tabindex="-1" role="listbox">
|
||||
{heading && <div className="dropdown__heading">{heading}</div>}
|
||||
<div className="dropdown__items">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { default } from './dropdown';
|
||||
export { default as DropdownItem } from './__item/dropdown__item';
|
||||
|
||||
require('./dropdown.scss');
|
||||
require('./_active/dropdown_active.scss');
|
||||
|
||||
require('./__item/dropdown__item.scss');
|
||||
require('./__items/dropdown__items.scss');
|
||||
require('./__title/dropdown__title.scss');
|
||||
require('./__content/dropdown__content.scss');
|
||||
@@ -7,10 +7,6 @@
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
|
||||
@@ -6,6 +6,7 @@ import { siteId, url } from 'common/settings';
|
||||
|
||||
import api from 'common/api';
|
||||
import store from 'common/store';
|
||||
import TextareaAutosize from 'components/input/textarea-autosize';
|
||||
|
||||
const RSS_THREAD_URL = `${BASE_URL}${API_BASE}/rss/post?site=${siteId}&url=${url}`;
|
||||
const RSS_SITE_URL = `${BASE_URL}${API_BASE}/rss/site?site=${siteId}`;
|
||||
@@ -21,7 +22,7 @@ export default class Input extends Component {
|
||||
isErrorShown: false,
|
||||
isDisabled: false,
|
||||
maxLength: config.max_comment_size || DEFAULT_MAX_COMMENT_SIZE,
|
||||
commentLength: 0,
|
||||
text: props.value || '',
|
||||
};
|
||||
|
||||
this.send = this.send.bind(this);
|
||||
@@ -31,28 +32,11 @@ export default class Input extends Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { mods = {}, value } = this.props;
|
||||
|
||||
if (this.props.autoFocus) {
|
||||
this.fieldNode.focus();
|
||||
}
|
||||
|
||||
if (mods.mode !== 'edit') {
|
||||
this.fieldNode.value = '';
|
||||
} else {
|
||||
this.fieldNode.value = value;
|
||||
this.autoResize();
|
||||
}
|
||||
|
||||
store.onUpdate('config', config => {
|
||||
this.setState({ maxLength: (config && config.max_comment_size) || DEFAULT_MAX_COMMENT_SIZE });
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.fieldNode.value = '';
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
return (
|
||||
nextProps.id !== this.props.id ||
|
||||
@@ -70,23 +54,16 @@ export default class Input extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onInput() {
|
||||
this.autoResize();
|
||||
|
||||
onInput(e) {
|
||||
this.setState({
|
||||
preview: null,
|
||||
isErrorShown: false,
|
||||
commentLength: this.fieldNode.value.length,
|
||||
text: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
autoResize() {
|
||||
this.fieldNode.style.height = '';
|
||||
this.fieldNode.style.height = `${this.fieldNode.scrollHeight}px`;
|
||||
}
|
||||
|
||||
send(e) {
|
||||
const text = this.fieldNode.value;
|
||||
const text = this.state.text;
|
||||
const { mods = {}, pid, id } = this.props;
|
||||
|
||||
if (e) e.preventDefault();
|
||||
@@ -104,9 +81,7 @@ export default class Input extends Component {
|
||||
this.props.onSubmit(comment);
|
||||
}
|
||||
|
||||
this.fieldNode.value = '';
|
||||
this.fieldNode.style.height = '';
|
||||
this.setState({ preview: null });
|
||||
this.setState({ preview: null, text: '' });
|
||||
})
|
||||
.catch(() => {
|
||||
this.setState({ isErrorShown: true });
|
||||
@@ -115,7 +90,7 @@ export default class Input extends Component {
|
||||
}
|
||||
|
||||
getPreview() {
|
||||
const text = this.fieldNode.value;
|
||||
const text = this.state.text;
|
||||
|
||||
if (!text || !text.trim()) return;
|
||||
|
||||
@@ -129,21 +104,20 @@ export default class Input extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
render(props, { isDisabled, isErrorShown, preview, maxLength, commentLength }) {
|
||||
const charactersLeft = maxLength - commentLength;
|
||||
const { mods = {}, value = null, errorMessage } = props;
|
||||
render(props, { isDisabled, isErrorShown, preview, maxLength, text }) {
|
||||
const charactersLeft = maxLength - text.length;
|
||||
const { mods = {}, errorMessage } = props;
|
||||
|
||||
return (
|
||||
<form className={b('input', props)} onSubmit={this.send} aria-label="New comment">
|
||||
<div className="input__field-wrapper">
|
||||
<textarea
|
||||
<TextareaAutosize
|
||||
className="input__field"
|
||||
placeholder="Your comment here"
|
||||
defaultValue={value}
|
||||
value={text}
|
||||
maxLength={maxLength}
|
||||
onInput={this.onInput}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={r => (this.fieldNode = r)}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @jsx h */
|
||||
import { h, Component } from 'preact';
|
||||
|
||||
export default class TextareaAutosize extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onRef = this.onRef.bind(this);
|
||||
}
|
||||
componentDidMount() {
|
||||
this.autoResize();
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.value !== this.props.value) {
|
||||
this.autoResize();
|
||||
}
|
||||
}
|
||||
onRef(node) {
|
||||
this.textareaRef = node;
|
||||
}
|
||||
autoResize() {
|
||||
this.textareaRef.style.height = '';
|
||||
this.textareaRef.style.height = `${this.textareaRef.scrollHeight}px`;
|
||||
}
|
||||
render(props) {
|
||||
return (
|
||||
// We set text as a child of textarea and not in value property for a reason.
|
||||
// It's a workaround for the bug described here https://github.com/developit/preact/issues/326
|
||||
<textarea {...props} ref={this.onRef}>
|
||||
{props.value}
|
||||
</textarea>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -18,8 +18,4 @@
|
||||
&:focus {
|
||||
background: #0aa;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ export default class Root extends Component {
|
||||
this.onSignOut = this.onSignOut.bind(this);
|
||||
this.onBlockedUsersShow = this.onBlockedUsersShow.bind(this);
|
||||
this.onBlockedUsersHide = this.onBlockedUsersHide.bind(this);
|
||||
this.onCommentsDisable = this.onCommentsDisable.bind(this);
|
||||
this.onCommentsEnable = this.onCommentsEnable.bind(this);
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
this.onUnblockSomeone = this.onUnblockSomeone.bind(this);
|
||||
this.checkUrlHash = this.checkUrlHash.bind(this);
|
||||
@@ -56,6 +58,7 @@ export default class Root extends Component {
|
||||
|
||||
componentWillMount() {
|
||||
store.onUpdate('comments', comments => this.setState({ comments }));
|
||||
store.onUpdate('info', info => this.setState({ info }));
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -73,7 +76,10 @@ export default class Root extends Component {
|
||||
.catch(() => store.set('user', {})),
|
||||
api
|
||||
.getPostComments({ sort, url })
|
||||
.then(({ comments = [] } = {}) => store.set('comments', comments))
|
||||
.then(({ comments = [], info = {} } = {}) => {
|
||||
store.set('comments', comments);
|
||||
store.set('info', info);
|
||||
})
|
||||
.catch(() => store.set('comments', [])),
|
||||
]).finally(() => {
|
||||
this.setState({
|
||||
@@ -111,18 +117,22 @@ export default class Root extends Component {
|
||||
|
||||
onSignIn(provider) {
|
||||
const newWindow = window.open(
|
||||
`${BASE_URL}/auth/${provider}/login?from=${encodeURIComponent(location.href)}&site=${siteId}`
|
||||
`${BASE_URL}/auth/${provider}/login?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
)}&site=${siteId}`
|
||||
);
|
||||
|
||||
let secondsPass = 0;
|
||||
const checkMsDelay = 100;
|
||||
const checkMsDelay = 300;
|
||||
const checkInterval = setInterval(() => {
|
||||
let shouldProceed;
|
||||
secondsPass += checkMsDelay;
|
||||
try {
|
||||
shouldProceed = newWindow.closed || secondsPass > 30000;
|
||||
} catch (e) {}
|
||||
|
||||
if (newWindow.location.origin === location.origin || secondsPass > 30000) {
|
||||
if (shouldProceed) {
|
||||
clearInterval(checkInterval);
|
||||
secondsPass = 0;
|
||||
newWindow.close();
|
||||
|
||||
api
|
||||
.getUser()
|
||||
@@ -141,12 +151,31 @@ export default class Root extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onCommentsEnable() {
|
||||
api.enableComments(siteId, url).then(() => {
|
||||
const info = store.get('info');
|
||||
info.read_only = false;
|
||||
this.setState({ info });
|
||||
});
|
||||
}
|
||||
|
||||
onCommentsDisable() {
|
||||
api.disableComments(siteId, url).then(() => {
|
||||
const info = store.get('info');
|
||||
info.read_only = true;
|
||||
this.setState({ info });
|
||||
});
|
||||
}
|
||||
|
||||
onBlockedUsersHide() {
|
||||
const { wasSomeoneUnblocked, sort } = this.state;
|
||||
|
||||
// if someone was unblocked let's reload comments
|
||||
if (wasSomeoneUnblocked) {
|
||||
api.getPostComments({ sort, url }).then(({ comments } = {}) => store.set('comments', comments));
|
||||
api.getPostComments({ sort, url }).then(({ comments, info } = {}) => {
|
||||
store.set('comments', comments);
|
||||
store.set('info', info);
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({
|
||||
@@ -168,7 +197,10 @@ export default class Root extends Component {
|
||||
|
||||
api
|
||||
.getPostComments({ sort, url })
|
||||
.then(({ comments } = {}) => store.set('comments', comments))
|
||||
.then(({ comments, info } = {}) => {
|
||||
store.set('comments', comments);
|
||||
store.set('info', info);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setState({ isCommentsListLoading: false });
|
||||
});
|
||||
@@ -197,6 +229,7 @@ export default class Root extends Component {
|
||||
{
|
||||
config = {},
|
||||
comments = [],
|
||||
info = {},
|
||||
user,
|
||||
sort,
|
||||
isLoaded,
|
||||
@@ -219,6 +252,7 @@ export default class Root extends Component {
|
||||
// TODO: i think we should do it on backend
|
||||
const pinnedComments = store.getPinnedComments();
|
||||
const isGuest = !Object.keys(user).length;
|
||||
const isCommentsDisabled = info != null && info.read_only === true;
|
||||
|
||||
return (
|
||||
<div id={NODE_ID}>
|
||||
@@ -227,16 +261,20 @@ export default class Root extends Component {
|
||||
user={user}
|
||||
sort={sort}
|
||||
providers={config.auth_providers}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
onSignIn={this.onSignIn}
|
||||
onSignOut={this.onSignOut}
|
||||
onBlockedUsersShow={this.onBlockedUsersShow}
|
||||
onBlockedUsersHide={this.onBlockedUsersHide}
|
||||
onCommentsEnable={this.onCommentsEnable}
|
||||
onCommentsDisable={this.onCommentsDisable}
|
||||
onSortChange={this.onSortChange}
|
||||
/>
|
||||
|
||||
{!isBlockedVisible && (
|
||||
<div className="root__main">
|
||||
{!isGuest && <Input mix="root__input" mods={{ type: 'main' }} onSubmit={this.addComment} />}
|
||||
{!isGuest &&
|
||||
!isCommentsDisabled && <Input mix="root__input" mods={{ type: 'main' }} onSubmit={this.addComment} />}
|
||||
|
||||
{!!pinnedComments.length && (
|
||||
<div className="root__pinned-comments" role="region" aria-label="Pinned comments">
|
||||
@@ -255,6 +293,7 @@ export default class Root extends Component {
|
||||
mix="root__thread"
|
||||
mods={{ level: 0 }}
|
||||
data={thread}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
onReply={this.addComment}
|
||||
onEdit={this.replaceComment}
|
||||
/>
|
||||
|
||||
@@ -21,6 +21,7 @@ class Thread extends Component {
|
||||
collapsed,
|
||||
data: { comment, replies = [] },
|
||||
mods = {},
|
||||
isCommentsDisabled,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -31,6 +32,7 @@ class Thread extends Component {
|
||||
>
|
||||
<Comment
|
||||
data={comment}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
mods={{ level: mods.level, collapsed }}
|
||||
onReply={props.onReply}
|
||||
onEdit={props.onEdit}
|
||||
@@ -43,6 +45,7 @@ class Thread extends Component {
|
||||
<ConnectedThread
|
||||
key={thread.comment.id}
|
||||
data={thread}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
mods={{ level: mods.level < 5 ? mods.level + 1 : mods.level }}
|
||||
onReply={props.onReply}
|
||||
onEdit={props.onEdit}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
.user-info__close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
cursor: pointer;
|
||||
color: #888;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,6 @@ export const userInfoReducers = { userComments, isLoadingUserComments };
|
||||
export { default } from './user-info';
|
||||
|
||||
require('./user-info.scss');
|
||||
|
||||
require('./__close/user-info__close.scss');
|
||||
require('./__id/user-info__id.scss');
|
||||
require('./__preloader/user-info__preloader.scss');
|
||||
require('./__title/user-info__title.scss');
|
||||
|
||||
@@ -3,8 +3,6 @@ import { h, Component } from 'preact';
|
||||
import { connect } from 'preact-redux';
|
||||
|
||||
import api from 'common/api';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
|
||||
import LastCommentsList from './last-comments-list';
|
||||
import Avatar from 'components/avatar-icon';
|
||||
import { fetchComments, completeFetchComments } from './user-info.actions';
|
||||
@@ -28,6 +26,20 @@ class UserInfo extends Component {
|
||||
.then(({ comments }) => completeFetchComments(id, comments))
|
||||
.catch(() => completeFetchComments(id, []));
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', this.globalOnKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.globalOnKeyDown);
|
||||
}
|
||||
|
||||
globalOnKeyDown(e) {
|
||||
// ESCAPE key pressed
|
||||
if (e.keyCode == 27) {
|
||||
const data = JSON.stringify({ isUserInfoShown: false });
|
||||
window.parent.postMessage(data, '*');
|
||||
}
|
||||
}
|
||||
|
||||
render(props) {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/* eslint-disable no-console */
|
||||
import { NODE_ID } from 'common/constants';
|
||||
import { approveDeleteMe } from 'common/api';
|
||||
import { token } from 'common/settings';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
function init() {
|
||||
const node = document.getElementById(NODE_ID);
|
||||
|
||||
if (!node) {
|
||||
console.error("Remark42: Can't find root node.");
|
||||
return;
|
||||
}
|
||||
approveDeleteMe(token).then(
|
||||
data =>
|
||||
(node.innerHTML = `
|
||||
<h3>User deleted successfully</h3>
|
||||
<pre>${JSON.stringify(data, null, 4)}</pre>`),
|
||||
err =>
|
||||
(node.innerHTML = `
|
||||
<h3>Something went wrong</h3>
|
||||
<pre>${err}</pre>`)
|
||||
);
|
||||
}
|
||||
+122
-14
@@ -35,7 +35,7 @@ function init() {
|
||||
|
||||
node.innerHTML = `
|
||||
<iframe
|
||||
src="${process.env.NODE_ENV === 'production' ? `${BASE_URL}/web` : ''}/iframe.html?${query}"
|
||||
src="${BASE_URL}/web/iframe.html?${query}"
|
||||
width="100%"
|
||||
frameborder="0"
|
||||
allowtransparency="true"
|
||||
@@ -51,31 +51,95 @@ function init() {
|
||||
const iframe = node.getElementsByTagName('iframe')[0];
|
||||
|
||||
window.addEventListener('message', receiveMessages);
|
||||
|
||||
window.addEventListener('hashchange', postHashToIframe);
|
||||
|
||||
document.addEventListener('click', postClickOutsideToIframe);
|
||||
setTimeout(postHashToIframe, 1000);
|
||||
|
||||
const remarkRootId = 'remark-km423lmfdslkm34';
|
||||
const userInfo = {
|
||||
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.style = `position: fixed; top: 0; right: 0; bottom: 0;width: 400px; transform: translate(400px, 0); transition: transform 0.4s ease-out; max-width: 100%`;
|
||||
this.node.id = remarkRootId + '-node';
|
||||
}
|
||||
if (!this.back) {
|
||||
this.back = document.createElement('div');
|
||||
this.back.style = `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;`;
|
||||
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 || ''}&isDefaultPicture=${user.isDefaultPicture || 0}`;
|
||||
this.node.innerHTML = `
|
||||
<iframe
|
||||
src="${process.env.NODE_ENV === 'production' ? `${BASE_URL}/web` : ''}/iframe.html?${queryUserInfo}"
|
||||
src="${BASE_URL}/web/iframe.html?${queryUserInfo}"
|
||||
width="100%"
|
||||
height="100%"
|
||||
frameborder="0"
|
||||
@@ -85,24 +149,62 @@ function init() {
|
||||
title="Remark42"
|
||||
verticalscrolling="no"
|
||||
horizontalscrolling="no"
|
||||
/>
|
||||
`;
|
||||
/>`;
|
||||
this.iframe = this.node.querySelector('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);
|
||||
setTimeout(() => {
|
||||
this.back.style.opacity = 1;
|
||||
this.node.style.transform = '';
|
||||
this.back.setAttribute('data-animation', '');
|
||||
this.node.setAttribute('data-animation', '');
|
||||
this.iframe.focus();
|
||||
}, 400);
|
||||
},
|
||||
close() {
|
||||
if (this.node) {
|
||||
this.node.style.transform = 'translate(400px, 0)';
|
||||
this.node.remove();
|
||||
this.onAnimationClose();
|
||||
this.node.removeAttribute('data-animation');
|
||||
}
|
||||
if (this.back) {
|
||||
this.back.style.opacity = 0;
|
||||
this.back.remove();
|
||||
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 = 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;
|
||||
t.node && t.node.remove();
|
||||
t.back && t.back.remove();
|
||||
t.style && t.style.remove();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -136,4 +238,10 @@ function init() {
|
||||
iframe.contentWindow.postMessage(JSON.stringify({ hash }), '*');
|
||||
}
|
||||
}
|
||||
|
||||
function postClickOutsideToIframe(e) {
|
||||
if (!iframe.contains(e.target)) {
|
||||
iframe.contentWindow.postMessage(JSON.stringify({ clickOutside: true }), '*');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-console */
|
||||
/** @jsx h */
|
||||
import { h, render } from 'preact';
|
||||
import 'preact/debug';
|
||||
|
||||
import { BASE_URL, DEFAULT_LAST_COMMENTS_MAX, LAST_COMMENTS_NODE_CLASSNAME } from './common/constants';
|
||||
|
||||
|
||||
+3
-7
@@ -3,6 +3,7 @@
|
||||
import loadPolyfills from 'common/polyfills';
|
||||
|
||||
import { h, render } from 'preact';
|
||||
import 'preact/debug';
|
||||
import { Provider } from 'preact-redux';
|
||||
import Root from './components/root';
|
||||
import UserInfo from 'components/user-info';
|
||||
@@ -44,16 +45,11 @@ function init() {
|
||||
if (params.page === 'user-info') {
|
||||
const user = {
|
||||
id: params.id,
|
||||
name: params.name || '',
|
||||
isDefaultPicture: params.isDefaultPicture,
|
||||
name: decodeURIComponent(params.name) || '',
|
||||
isDefaultPicture: params.isDefaultPicture != 0,
|
||||
picture: params.picture,
|
||||
};
|
||||
store.set('user', user);
|
||||
const onClose = () => {
|
||||
if (window.parent) {
|
||||
window.parent.postMessage(JSON.stringify({ isUserInfoShown: false }), '*');
|
||||
}
|
||||
};
|
||||
render(
|
||||
<div id={NODE_ID}>
|
||||
<div className="root root_user-info">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
module.exports = {};
|
||||
@@ -0,0 +1,35 @@
|
||||
import store from '../common/store';
|
||||
import { siteId } from '../common/settings';
|
||||
import { deleteMe } from '../common/api';
|
||||
|
||||
// The right line breaks code in the body of inline email
|
||||
// should be not just %0A, but %0D%0A
|
||||
// see: https://www.ietf.org/rfc/rfc2368.txt
|
||||
const LINE_BREAK_CODE = '%0D%0A';
|
||||
|
||||
export function getDeleteInformationMessage(userId, siteId, link) {
|
||||
const subject = encodeURIComponent("Request to delete user's information");
|
||||
const message = encodeURIComponent(`Request to delete all information about ${userId} from remark42 on ${siteId}
|
||||
|
||||
[you can provide the reason for removal request, optional]
|
||||
|
||||
=== DO NOT REMOVE THE TEXT BELOW THIS LINE ===
|
||||
|
||||
site: ${siteId}
|
||||
user: ${userId}
|
||||
link: ${link}
|
||||
`).replace('%0A', LINE_BREAK_CODE);
|
||||
|
||||
return {
|
||||
subject,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
export function requestDeletion() {
|
||||
return deleteMe().then(data => {
|
||||
const email = store.get('config').admin_email;
|
||||
const { subject, message } = getDeleteInformationMessage(data.user_id, siteId, data.link);
|
||||
window.location = `mailto:${email}?subject=${subject}&body=${message}`;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export default function noop() {}
|
||||
+5
-7
@@ -21,14 +21,14 @@
|
||||
<div class="container">
|
||||
<p>
|
||||
First counter with url from data-attribute:
|
||||
<span class="remark42__counter" data-url="https://radio-t.com/p/2017/11/11/podcast-571/"></span>
|
||||
(<a href="https://radio-t.com/p/2017/11/11/podcast-571/" target="_blank">note</a>)
|
||||
<span class="remark42__counter" data-url="http://127.0.0.1:8080/web/"></span>
|
||||
(<a href="http://127.0.0.1:8080/web/" target="_blank">note</a>)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Second counter with url from global remark config:
|
||||
<span class="remark42__counter"></span>
|
||||
(<a href="https://radio-t.com/p/2017/12/16/podcast-576/" target="_blank">note</a>)
|
||||
(<a href="http://127.0.0.1:8080/web/" target="_blank">note</a>)
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@@ -38,14 +38,12 @@
|
||||
|
||||
<script>
|
||||
var remark_config = {
|
||||
site_id: 'radiot',
|
||||
url: 'https://radio-t.com/p/2017/12/16/podcast-576/'
|
||||
site_id: 'remark',
|
||||
};
|
||||
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
var baseurl = '';
|
||||
s.src = baseurl + '/web/counter.js';
|
||||
s.src = '/web/counter.js';
|
||||
s.type = 'text/javascript';
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>remark42</title>
|
||||
<base target="_blank">
|
||||
<style>
|
||||
html, body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.preloader {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.preloader_view_iframe {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.preloader__bounce {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
margin-right: 3px;
|
||||
background-color: #333;
|
||||
border-radius: 100%;
|
||||
animation: iframePreloaderBounce 1.4s infinite ease-in-out both;
|
||||
}
|
||||
|
||||
.preloader__bounce:first-child {
|
||||
animation-delay: -.32s;
|
||||
}
|
||||
|
||||
.preloader__bounce:nth-child(2) {
|
||||
animation-delay: -.16s;
|
||||
}
|
||||
|
||||
.preloader__bounce:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@keyframes iframePreloaderBounce {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
80%, 100% {
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="remark42">
|
||||
<div class="preloader preloader_view_iframe">
|
||||
<div class="preloader__bounce"></div>
|
||||
<div class="preloader__bounce"></div>
|
||||
<div class="preloader__bounce"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="deleteme.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>remark42</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.container {
|
||||
margin: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div id="remark42"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var remark_config = {
|
||||
site_id: 'radiot',
|
||||
url: 'https://radio-t.com/p/2017/12/16/podcast-576/',
|
||||
};
|
||||
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
s.src = '/embed.js';
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+17
-1
@@ -1,10 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>remark42</title>
|
||||
<base target="_blank">
|
||||
<script>
|
||||
if (window.location.search === '?selfClose') {
|
||||
window.close();
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="remark.css">
|
||||
<style>
|
||||
html, body {
|
||||
@@ -59,6 +64,10 @@
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
:focus:not(.focus-visible):not(.button) {
|
||||
outline: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -70,6 +79,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
/* REMOVE-START */
|
||||
if (window.parent !== window) {
|
||||
try {
|
||||
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
||||
} catch (e){}
|
||||
}
|
||||
/* REMOVE-END */
|
||||
var lastHeight = 0;
|
||||
setInterval(function() {
|
||||
if (document.body.offsetHeight !== lastHeight) {
|
||||
|
||||
+8
-2
@@ -19,6 +19,13 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Demo page</h1>
|
||||
<p>To install widgets on your website, follow the <a href="https://github.com/umputun/remark#setup-on-your-website">instructions</a>.</p>
|
||||
<p>See also</p>
|
||||
<ul>
|
||||
<li><a href="/web/last-comments.html">Last comments widget</a></li>
|
||||
<li><a href="/web/counter.html">Counter widget</a></li>
|
||||
</ul>
|
||||
<div id="remark42"></div>
|
||||
</div>
|
||||
|
||||
@@ -30,8 +37,7 @@
|
||||
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
var baseurl = 'https://demo.remark42.com/';
|
||||
s.src = baseurl + '/web/embed.js';
|
||||
s.src = '/web/embed.js';
|
||||
s.type = 'text/javascript';
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
|
||||
@@ -24,13 +24,12 @@
|
||||
|
||||
<script>
|
||||
var remark_config = {
|
||||
site_id: 'radiot',
|
||||
site_id: 'remark',
|
||||
};
|
||||
|
||||
(function() {
|
||||
var d = document, s = d.createElement('script');
|
||||
var baseurl = '';
|
||||
s.src = baseurl + '/web/last-comments.js';
|
||||
s.src = '/web/last-comments.js';
|
||||
s.type = 'text/javascript';
|
||||
(d.head || d.body).appendChild(s);
|
||||
})();
|
||||
|
||||
Generated
+623
-200
File diff suppressed because it is too large
Load Diff
+10
-7
@@ -2,7 +2,7 @@
|
||||
"name": "remark-ui",
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js",
|
||||
"build": "webpack --config ./webpack.config.js",
|
||||
"start": "webpack-dev-server --progress --hot --inline --config ./webpack.config.js",
|
||||
"lint": "eslint --ext=.js,.jsx .",
|
||||
"test": "jest",
|
||||
@@ -31,8 +31,6 @@
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"clean-webpack-plugin": "^0.1.19",
|
||||
"copy-webpack-plugin": "^4.5.1",
|
||||
"core-js": "^2.5.7",
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^0.28.11",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-prettier": "^2.9.0",
|
||||
@@ -55,9 +53,7 @@
|
||||
"postcss-simple-vars": "^4.1.0",
|
||||
"postcss-url": "^6.3.1",
|
||||
"postcss-wrap": "0.0.4",
|
||||
"preact-redux": "^2.0.3",
|
||||
"prettier": "^1.13.7",
|
||||
"redux": "^4.0.0",
|
||||
"style-loader": "^0.19.1",
|
||||
"webpack": "^3.12.0",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
@@ -66,7 +62,11 @@
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"bem-react-helper": "^1.1.2",
|
||||
"preact": "^8.2.9"
|
||||
"core-js": "^2.5.7",
|
||||
"focus-visible": "^4.1.5",
|
||||
"preact": "^8.2.9",
|
||||
"preact-redux": "^2.0.3",
|
||||
"redux": "^4.0.0"
|
||||
},
|
||||
"eslintIgnore": [
|
||||
"public"
|
||||
@@ -85,7 +85,10 @@
|
||||
],
|
||||
"testMatch": [
|
||||
"<rootDir>/**/*.test.js"
|
||||
]
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"\\.scss$": "<rootDir>/app/testUtils/mockStyles.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
|
||||
+30
-25
@@ -11,12 +11,13 @@ const Define = webpack.DefinePlugin;
|
||||
const BundleAnalyze = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
|
||||
|
||||
const babelOptions = require('./babelOptions');
|
||||
const { NODE_ID } = require('./app/common/constants');
|
||||
const publicFolder = path.resolve(__dirname, 'public');
|
||||
const env = process.env.NODE_ENV || 'dev';
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const remarkUrl = process.env.REMARK_URL || 'https://demo.remark42.com';
|
||||
const NODE_ID = 'remark42';
|
||||
// let's log some env variables because we can
|
||||
console.log(`NODE_ENV = ${env}`);
|
||||
console.log(`REMARK_ENV = ${remarkUrl}`);
|
||||
|
||||
const commonStyleLoaders = [
|
||||
'css-loader',
|
||||
@@ -39,11 +40,13 @@ const commonStyleLoaders = [
|
||||
|
||||
module.exports = {
|
||||
context: __dirname,
|
||||
devtool: env === 'development' ? 'source-map' : false,
|
||||
entry: {
|
||||
embed: './app/embed',
|
||||
counter: './app/counter',
|
||||
'last-comments': './app/last-comments',
|
||||
remark: './app/remark',
|
||||
deleteme: './app/deleteme',
|
||||
},
|
||||
output: {
|
||||
path: publicFolder,
|
||||
@@ -66,13 +69,10 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use:
|
||||
env === 'production'
|
||||
? ExtractText.extract({
|
||||
fallback: 'style-loader',
|
||||
use: commonStyleLoaders,
|
||||
})
|
||||
: ['style-loader', ...commonStyleLoaders],
|
||||
use: ExtractText.extract({
|
||||
fallback: 'style-loader',
|
||||
use: commonStyleLoaders,
|
||||
}),
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|svg)$/,
|
||||
@@ -91,7 +91,9 @@ module.exports = {
|
||||
b: 'bem-react-helper',
|
||||
}),
|
||||
new Define({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.NODE_ENV': JSON.stringify(env),
|
||||
'process.env.REMARK_NODE': JSON.stringify(NODE_ID),
|
||||
'process.env.REMARK_URL': env === 'production' ? JSON.stringify(remarkUrl) : 'window.location.origin',
|
||||
}),
|
||||
// TODO: we should add it only on demo serv
|
||||
new Html({
|
||||
@@ -110,22 +112,13 @@ module.exports = {
|
||||
filename: 'last-comments.html',
|
||||
inject: false,
|
||||
}),
|
||||
...(env === 'production'
|
||||
? []
|
||||
: [
|
||||
new Html({
|
||||
template: path.resolve(__dirname, 'dev.ejs'),
|
||||
filename: 'dev.html',
|
||||
inject: false,
|
||||
}),
|
||||
]),
|
||||
new ExtractText({
|
||||
filename: `remark.css`,
|
||||
allChunks: true,
|
||||
}),
|
||||
new webpack.optimize.ModuleConcatenationPlugin(),
|
||||
...(env === 'production' ? [new webpack.optimize.UglifyJsPlugin()] : []),
|
||||
...(process.env.CI === 'true'
|
||||
...(process.env.CI
|
||||
? []
|
||||
: [
|
||||
new BundleAnalyze({
|
||||
@@ -137,15 +130,27 @@ module.exports = {
|
||||
openAnalyzer: false,
|
||||
}),
|
||||
]),
|
||||
new Copy(['./iframe.html']),
|
||||
new Copy(['./iframe.html', './deleteme.html']),
|
||||
],
|
||||
watch: env === 'dev',
|
||||
watchOptions: {
|
||||
ignored: /(node_modules|\.vendor\.js$)/,
|
||||
},
|
||||
devServer: {
|
||||
host: '0.0.0.0',
|
||||
port: 8080,
|
||||
host: 'localhost',
|
||||
port: 9000,
|
||||
contentBase: publicFolder,
|
||||
publicPath: '/web',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: remarkUrl,
|
||||
logLevel: 'debug',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/auth': {
|
||||
target: remarkUrl,
|
||||
logLevel: 'debug',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user