Merge branch 'master' into user-info-redux

This commit is contained in:
Igor Adamenko
2018-09-29 22:50:33 +03:00
committed by GitHub
255 changed files with 30505 additions and 2092 deletions
+1 -1
View File
@@ -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,
+26
View File
@@ -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
View File
@@ -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,
};
+10 -2
View File
@@ -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
View File
@@ -1,4 +1,5 @@
import 'core-js/es7/promise';
import 'focus-visible';
export default function loadPolyfills() {
const fillCoreJs = () => {
+1
View File
@@ -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;
}
+50 -14
View File
@@ -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>
)}
+2 -1
View File
@@ -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;
}
}
+69
View File
@@ -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,
};
+6
View File
@@ -0,0 +1,6 @@
.button {
cursor: pointer;
outline-width: 0;
font-size: inherit;
font-family: inherit;
}
+5
View File
@@ -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');
+4 -2
View File
@@ -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;
}
}
+87
View File
@@ -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;
}
+10
View File
@@ -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;
+12 -38
View File
@@ -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 -9
View File
@@ -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}
/>
+3
View File
@@ -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;
}
}
-2
View File
@@ -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');
+14 -2
View File
@@ -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) {
+29
View File
@@ -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
View File
@@ -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 = '&#10006;';
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
View File
@@ -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
View File
@@ -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">
+1
View File
@@ -0,0 +1 @@
module.exports = {};
+35
View File
@@ -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}`;
});
}
+1
View File
@@ -0,0 +1 @@
export default function noop() {}
+5 -7
View File
@@ -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);
})();
+71
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
})();
+2 -3
View File
@@ -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);
})();
+623 -200
View File
File diff suppressed because it is too large Load Diff
+10 -7
View File
@@ -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
View File
@@ -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,
},
},
},
};