Merge branch 'master' into vote
This commit is contained in:
@@ -38,5 +38,6 @@ services:
|
||||
- ADMIN_SHARED_ID=dev_user # set admin flag for default user on local ouath2
|
||||
- POSITIVE_SCORE=false # restricts comment's score to be only positive
|
||||
- EDIT_TIME=5m # edit window
|
||||
- AUTH_ANON=true
|
||||
volumes:
|
||||
- ./var:/srv/var
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@
|
||||
"targets": {
|
||||
"browsers": ["> 1%", "android >= 4.4.4", "ios >= 9", "IE >= 11"]
|
||||
},
|
||||
"useBuiltIns": "usage"
|
||||
"useBuiltIns": "usage",
|
||||
"corejs": 3
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
const handleBtnKeyPress = (event: KeyboardEvent, handler?: () => void) => {
|
||||
const handleBtnKeyPress = (event: KeyboardEvent, handler?: (e: KeyboardEvent | MouseEvent) => void) => {
|
||||
if (event.key === ' ' || event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
handler && handler();
|
||||
handler && handler(event);
|
||||
}
|
||||
};
|
||||
|
||||
export const getHandleClickProps = (handler?: () => void) => ({
|
||||
export const getHandleClickProps = (handler?: (e: KeyboardEvent | MouseEvent) => void) => ({
|
||||
role: 'button',
|
||||
onClick: handler,
|
||||
onKeyPress: (event: KeyboardEvent) => handleBtnKeyPress(event, handler),
|
||||
|
||||
+16
-7
@@ -1,17 +1,26 @@
|
||||
import { siteId, url } from './settings';
|
||||
import { BASE_URL } from './constants';
|
||||
import { Config, Comment, Tree, User, BlockedUser, Sorting, Provider, BlockTTL } from './types';
|
||||
import { Config, Comment, Tree, User, BlockedUser, Sorting, AuthProvider, BlockTTL } from './types';
|
||||
import fetcher from './fetcher';
|
||||
|
||||
/* common */
|
||||
|
||||
export const logIn = (provider: Provider) => {
|
||||
const __loginAnonymously = (username: string): Promise<User | null> => {
|
||||
const url = `/auth/anonymous/login?user=${encodeURIComponent(username)}&aud=${siteId}?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
)}`;
|
||||
return fetcher.get<User>({ url, withCredentials: true, overriddenApiBase: '' });
|
||||
};
|
||||
|
||||
export const logIn = (provider: AuthProvider): Promise<User | null> => {
|
||||
if (provider.name === 'anonymous') return __loginAnonymously(provider.username);
|
||||
|
||||
return new Promise<User | null>((resolve, reject) => {
|
||||
const newWindow = window.open(
|
||||
`${BASE_URL}/auth/${provider}/login?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
)}&site=${siteId}`
|
||||
);
|
||||
const url = `${BASE_URL}/auth/${provider.name}/login?from=${encodeURIComponent(
|
||||
location.origin + location.pathname + '?selfClose'
|
||||
)}&site=${siteId}`;
|
||||
|
||||
const newWindow = window.open(url);
|
||||
|
||||
let secondsPass = 0;
|
||||
const checkMsDelay = 300;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Sorting, Provider, BlockingDuration, Theme } from './types';
|
||||
import { Sorting, AuthProvider, BlockingDuration, Theme } from './types';
|
||||
|
||||
export const BASE_URL: string = process.env.REMARK_URL!;
|
||||
export const API_BASE = '/api/v1';
|
||||
@@ -12,13 +12,14 @@ export const MAX_SHOWN_ROOT_COMMENTS = 10;
|
||||
|
||||
export const DEFAULT_SORT: Sorting = '-active';
|
||||
|
||||
/* object of supported providers */
|
||||
export const PROVIDER_NAMES: { [P in Provider]: string } = {
|
||||
/* matches auth providers to UI label */
|
||||
export const PROVIDER_NAMES: { [P in AuthProvider['name']]: string } = {
|
||||
google: 'Google',
|
||||
facebook: 'Facebook',
|
||||
github: 'GitHub',
|
||||
yandex: 'Yandex',
|
||||
dev: 'Dev',
|
||||
anonymous: 'Anonymous',
|
||||
};
|
||||
|
||||
/** locastorage key for collapsed comments */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'core-js/es7/promise';
|
||||
import 'core-js/es/promise';
|
||||
import 'focus-visible';
|
||||
|
||||
export default async function loadPolyfills() {
|
||||
|
||||
@@ -92,7 +92,7 @@ export interface Config {
|
||||
max_comment_size: number;
|
||||
admins: string[];
|
||||
admin_email: string;
|
||||
auth_providers: Provider[];
|
||||
auth_providers: (AuthProvider['name'])[];
|
||||
low_score: number;
|
||||
critical_score: number;
|
||||
positive_score: boolean;
|
||||
@@ -108,7 +108,13 @@ export interface RemarkConfig {
|
||||
|
||||
export type Sorting = '-time' | '+time' | '-active' | '+active' | '-score' | '+score' | '-controversy' | '+controversy';
|
||||
|
||||
export type Provider = 'google' | 'facebook' | 'github' | 'yandex' | 'dev';
|
||||
export type AuthProvider =
|
||||
| { name: 'google' }
|
||||
| { name: 'facebook' }
|
||||
| { name: 'github' }
|
||||
| { name: 'yandex' }
|
||||
| { name: 'dev' }
|
||||
| { name: 'anonymous'; username: string };
|
||||
|
||||
export type BlockTTL = 'permanently' | '43200m' | '10080m' | '1440m';
|
||||
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
.auth-panel-anonymous-login-form {
|
||||
padding: 0.5em 0.7em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.auth-panel-anonymous-login-form__input {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
.auth-panel-anonymous-login-form__remember-me {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-panel-anonymous-login-form__submit {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0 0.5em;
|
||||
color: currentColor;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-panel-anonymous-login-form__submit:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
|
||||
interface Props {
|
||||
onSubmit(username: string): Promise<void>;
|
||||
theme: Theme;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
inputValue: string;
|
||||
honeyPotValue: boolean;
|
||||
}
|
||||
|
||||
export class AnonymousLoginForm extends Component<Props, State> {
|
||||
static usernameRegex = /^[a-zA-Z][\w ]+$/;
|
||||
|
||||
inputRef?: HTMLInputElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
inputValue: '',
|
||||
honeyPotValue: false,
|
||||
};
|
||||
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onCheckedChange = this.onCheckedChange.bind(this);
|
||||
}
|
||||
|
||||
onSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
if (this.state.honeyPotValue) {
|
||||
// what should i do if bot uncovered?
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
this.props.onSubmit(this.state.inputValue);
|
||||
}
|
||||
|
||||
onChange(e: Event) {
|
||||
this.setState({ inputValue: (e.target as HTMLInputElement).value });
|
||||
}
|
||||
|
||||
getUsernameInvalidReason(): string | null {
|
||||
const value = this.state.inputValue;
|
||||
if (value.length < 3) return 'Username must be at least 3 characters long';
|
||||
if (!AnonymousLoginForm.usernameRegex.test(value))
|
||||
return 'Username must start from the letter and contain only latin letters, numbers, underscores, and spaces';
|
||||
return null;
|
||||
}
|
||||
|
||||
onCheckedChange(e: Event) {
|
||||
this.setState({ honeyPotValue: (e.target as HTMLInputElement).checked });
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(() => {
|
||||
this.inputRef && this.inputRef.focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>) {
|
||||
// TODO: will be great to `b` to accept `string | undefined | (string|undefined)[]` as classname
|
||||
let className = b('auth-panel-anonymous-login-form', {}, { theme: props.theme });
|
||||
if (props.className) {
|
||||
className += ' ' + b('auth-panel-anonymous-login-form', {}, { theme: props.theme });
|
||||
}
|
||||
|
||||
const usernameInvalidReason = this.getUsernameInvalidReason();
|
||||
|
||||
return (
|
||||
<form className={className} onSubmit={this.onSubmit}>
|
||||
<input
|
||||
className="auth-panel-anonymous-login-form__input"
|
||||
ref={ref => (this.inputRef = ref)}
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={this.state.inputValue}
|
||||
onInput={this.onChange}
|
||||
/>
|
||||
{/* honeypot input */}
|
||||
<input
|
||||
className="auth-panel-anonymous-login-form__remember-me"
|
||||
type="checkbox"
|
||||
tabIndex={-1}
|
||||
autocomplete="off"
|
||||
onChange={this.onCheckedChange}
|
||||
checked={this.state.honeyPotValue}
|
||||
/>
|
||||
<input
|
||||
className="auth-panel-anonymous-login-form__submit"
|
||||
type="submit"
|
||||
value="Log in"
|
||||
title={usernameInvalidReason || ''}
|
||||
disabled={usernameInvalidReason !== null}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { AnonymousLoginForm } from './auth-panel__anonymous-login-form';
|
||||
|
||||
require('./auth-panel__anonymous-login-form.scss');
|
||||
@@ -3,9 +3,11 @@
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
color: #0aa;
|
||||
/* important so "anonymous" label in Dropdown component
|
||||
will not be overriden by `.dropdown_theme_light .dropdown__title` style */
|
||||
color: #0aa !important;
|
||||
|
||||
&:hover {
|
||||
color: #06c5c5;
|
||||
color: #06c5c5 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
.auth-panel__user-id {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 5px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
import { h } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import { Theme } from '@app/common/types';
|
||||
import { exclude } from '@app/utils/exclude';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
export const UserID = (props: Props) => (
|
||||
<div className={b('auth-panel__user-id', {}, { theme: props.theme })} title={props.id}>
|
||||
export const UserID = (props: JSX.HTMLAttributes & Props) => (
|
||||
<div
|
||||
{...exclude(props, 'id', 'theme')}
|
||||
className={b('auth-panel__user-id', {}, { theme: props.theme })}
|
||||
title={props.id}
|
||||
>
|
||||
{props.id}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { User, PostInfo } from '../../common/types';
|
||||
|
||||
const DefaultProps: Partial<Props> = {
|
||||
sort: '-score',
|
||||
providers: [`google`, `github`],
|
||||
providers: ['google', 'github'],
|
||||
postInfo: {
|
||||
read_only: false,
|
||||
url: 'https://example.com',
|
||||
|
||||
@@ -5,22 +5,23 @@ import b from 'bem-react-helper';
|
||||
import { PROVIDER_NAMES, IS_STORAGE_AVAILABLE, IS_THIRD_PARTY } from '@app/common/constants';
|
||||
import { requestDeletion } from '@app/utils/email';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { User, Provider, Sorting, Theme, PostInfo } from '@app/common/types';
|
||||
import { User, AuthProvider, Sorting, Theme, PostInfo } from '@app/common/types';
|
||||
|
||||
import Dropdown, { DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import { UserID } from './__user-id';
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
|
||||
export interface Props {
|
||||
user: User | null;
|
||||
providers: Provider[];
|
||||
providers: (AuthProvider['name'])[];
|
||||
sort: Sorting;
|
||||
isCommentsDisabled: boolean;
|
||||
theme: Theme;
|
||||
postInfo: PostInfo;
|
||||
|
||||
onSortChange(s: Sorting): Promise<void>;
|
||||
onSignIn(p: Provider): Promise<User | null>;
|
||||
onSignIn(p: AuthProvider): Promise<User | null>;
|
||||
onSignOut(): Promise<void>;
|
||||
onCommentsEnable(): Promise<boolean>;
|
||||
onCommentsDisable(): Promise<boolean>;
|
||||
@@ -30,6 +31,7 @@ export interface Props {
|
||||
|
||||
interface State {
|
||||
isBlockedVisible: boolean;
|
||||
anonymousUsernameInputValue: string;
|
||||
}
|
||||
|
||||
export class AuthPanel extends Component<Props, State> {
|
||||
@@ -38,11 +40,16 @@ export class AuthPanel extends Component<Props, State> {
|
||||
|
||||
this.state = {
|
||||
isBlockedVisible: false,
|
||||
anonymousUsernameInputValue: 'anon',
|
||||
};
|
||||
|
||||
this.toggleBlockedVisibility = this.toggleBlockedVisibility.bind(this);
|
||||
this.toggleCommentsAvailability = this.toggleCommentsAvailability.bind(this);
|
||||
this.onSortChange = this.onSortChange.bind(this);
|
||||
this.onSignIn = this.onSignIn.bind(this);
|
||||
this.handleAnonymousLoginFormSubmut = this.handleAnonymousLoginFormSubmut.bind(this);
|
||||
this.handleOAuthLogin = this.handleOAuthLogin.bind(this);
|
||||
this.toggleUserInfoVisibility = this.toggleUserInfoVisibility.bind(this);
|
||||
}
|
||||
|
||||
onSortChange(e: Event) {
|
||||
@@ -67,16 +74,40 @@ export class AuthPanel extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
toggleUserInfoVisibility() {
|
||||
const user = this.props.user;
|
||||
if (window.parent && user) {
|
||||
const data = JSON.stringify({ isUserInfoShown: true, user });
|
||||
window.parent.postMessage(data, '*');
|
||||
}
|
||||
}
|
||||
|
||||
getUserTitle() {
|
||||
const { user } = this.props;
|
||||
return <span className="auth-panel__username">{user!.name}</span>;
|
||||
}
|
||||
|
||||
/** wrapper function to handle both oauth and anonymous providers*/
|
||||
onSignIn(provider: AuthProvider) {
|
||||
this.props.onSignIn(provider);
|
||||
}
|
||||
|
||||
async handleAnonymousLoginFormSubmut(username: string) {
|
||||
this.onSignIn({ name: 'anonymous', username });
|
||||
}
|
||||
|
||||
async handleOAuthLogin(e: MouseEvent | KeyboardEvent) {
|
||||
const p = (e.target as HTMLButtonElement).dataset.provider! as AuthProvider['name'];
|
||||
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
|
||||
this.onSignIn({ name: p } as AuthProvider);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, { isBlockedVisible }: State) {
|
||||
const { user, providers = [], sort, isCommentsDisabled } = props;
|
||||
const sortArray = getSortArray(sort);
|
||||
const loggedIn = !!user;
|
||||
const signInMessage = props.postInfo.read_only ? 'Sign in using ' : 'Sign in to comment using ';
|
||||
const isUserAnonymous = user && user.id.substr(0, 10) === 'anonymous_';
|
||||
|
||||
return (
|
||||
<div className={b('auth-panel', {}, { theme: props.theme, loggedIn })}>
|
||||
@@ -84,19 +115,21 @@ export class AuthPanel extends Component<Props, State> {
|
||||
<div className="auth-panel__column">
|
||||
You signed in as{' '}
|
||||
<Dropdown title={user.name} theme={this.props.theme}>
|
||||
<DropdownItem separator={true}>
|
||||
<UserID id={user.id} theme={this.props.theme} />
|
||||
<DropdownItem separator={!isUserAnonymous}>
|
||||
<UserID id={user.id} theme={this.props.theme} {...getHandleClickProps(this.toggleUserInfoVisibility)} />
|
||||
</DropdownItem>
|
||||
|
||||
<DropdownItem>
|
||||
<Button
|
||||
kind="link"
|
||||
theme={this.props.theme}
|
||||
onClick={() => requestDeletion().then(() => props.onSignOut())}
|
||||
>
|
||||
Request my data removal
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
{!isUserAnonymous && (
|
||||
<DropdownItem>
|
||||
<Button
|
||||
kind="link"
|
||||
theme={this.props.theme}
|
||||
onClick={() => requestDeletion().then(() => props.onSignOut())}
|
||||
>
|
||||
Request my data removal
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
)}
|
||||
</Dropdown>{' '}
|
||||
<Button
|
||||
className="auth-panel__sign-out"
|
||||
@@ -115,12 +148,35 @@ export class AuthPanel extends Component<Props, State> {
|
||||
{providers.map((provider, i) => {
|
||||
const comma = i === 0 ? '' : i === providers.length - 1 ? ' or ' : ', ';
|
||||
|
||||
if (provider === 'anonymous') {
|
||||
return (
|
||||
<span>
|
||||
{comma}{' '}
|
||||
<Dropdown
|
||||
title={PROVIDER_NAMES[provider]}
|
||||
titleClass="auth-panel__pseudo-link"
|
||||
theme={this.props.theme}
|
||||
>
|
||||
<DropdownItem>
|
||||
<AnonymousLoginForm
|
||||
onSubmit={this.handleAnonymousLoginFormSubmut}
|
||||
theme={this.props.theme}
|
||||
className="auth-panel__anonymous-login-form"
|
||||
/>
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{comma}
|
||||
<span
|
||||
className="auth-panel__pseudo-link"
|
||||
{...getHandleClickProps(() => props.onSignIn(provider))}
|
||||
data-provider={provider}
|
||||
// eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion
|
||||
{...getHandleClickProps(this.handleOAuthLogin)}
|
||||
role="link"
|
||||
>
|
||||
{PROVIDER_NAMES[provider]}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="avatar-icon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(4.000000, 2.000000)">
|
||||
<g id="bg" transform="translate(0.600000, 0.497571)" fill-rule="nonzero">
|
||||
<path d="M1.0658141e-14,18.0024291 C0.9,18.0024291 14.2,18.1024291 15.1,18.1024291 L14.4,14.6024291 C14.2,13.3024291 14.1,11.9024291 14.1,10.6024291 L14.1,7.00242909 C14.1,3.40242909 11.2,0.102429093 7.6,0.00242909307 C3.8,-0.0975709069 0.7,2.90242909 0.7,6.70242909 L0.7,10.6024291 C0.7,11.9024291 0.6,13.3024291 0.4,14.6024291 L1.0658141e-14,18.0024291 Z" id="Path" fill="#F5F4F4"></path>
|
||||
<path d="M3.6,10.6024291 L3.6,6.70242909 C3.6,3.10242909 5.2,0.202429093 7.3,0.00242909307 C3.7,0.102429093 0.8,3.00242909 0.8,6.70242909 L0.8,10.6024291 C0.8,11.9024291 0.7,13.3024291 0.5,14.6024291 L5.77315973e-15,18.1024291 C0.9,18.1024291 2.62326389,17.9024291 3.02326389,18.1024291 L3.4,14.6024291 C3.5,13.3024291 3.6,11.9024291 3.6,10.6024291 Z" id="Path" fill="#FFFFFF" opacity="0.5"></path>
|
||||
</g>
|
||||
<path d="M15,11.1 L15,7.4 C15,3.6 12.1,0.4 8.4,0.1 C6.5,1.2490009e-16 4.6,0.7 3.2,2 C1.8,3.4 0.9,5.2 0.9,7.2 L0.9,11.1 C0.9,12.4 0.8,13.7 0.6,15 L0.1,18.5 C0.1,18.6 0.1,18.7 0.2,18.8 C0.3,18.9 0.4,19 0.5,19 C1.1,19 15.1,19 15.7,19 C15.8,19 15.9,19 16,18.9 C16.1,18.8 16.1,18.7 16.1,18.6 L15.3,15 C15.1,13.7 15,12.4 15,11.1 Z M1,18 L1.4,15.1 C1.6,13.8 1.7,12.4 1.7,11.1 L1.7,7.2 C1.7,5.4 2.4,3.8 3.7,2.6 C5,1.4 6.6,0.8 8.4,0.9 C11.7,1.1 14.3,4 14.3,7.4 L14.3,11 C14.3,12.3 14.4,13.7 14.6,15 L15,18 C14.9,18 1.1,18 1,18 Z" id="Shape" fill="#8ED0CF" fill-rule="nonzero"></path>
|
||||
<g id="tie" transform="translate(1.000000, 12.000000)" stroke="#8ED0CF">
|
||||
<polygon id="Triangle" stroke-width="0.9" fill="#8ED0CF" transform="translate(7.000000, 4.500000) scale(1, -1) translate(-7.000000, -4.500000) " points="7 3 9 6 5 6"></polygon>
|
||||
<path d="M5,3 L3.5,6.5" id="Line-4" stroke-linecap="round"></path>
|
||||
<path d="M9,3 L13.5,0.5" id="Line" stroke-linecap="round"></path>
|
||||
<path d="M5,3 L0.5,0.5" id="Line-2" stroke-linecap="round"></path>
|
||||
<path d="M9,3 L10.5,6.5" id="Line-3" stroke-linecap="round"></path>
|
||||
</g>
|
||||
<g id="eyes" transform="translate(4.000000, 5.000000)" fill="#8ED0CF" fill-rule="nonzero">
|
||||
<path d="M1.7,0.6 C1.1,0.6 0.9,1.4 0.9,2.2 C0.9,3 1.1,3.8 1.7,3.8 C2.3,3.8 2.5,3 2.5,2.2 C2.5,1.4 2.2,0.6 1.7,0.6 Z" id="Path"></path>
|
||||
<path d="M6.4,0.6 C5.8,0.6 5.6,1.4 5.6,2.2 C5.6,3 5.8,3.8 6.4,3.8 C7,3.8 7.2,3 7.2,2.2 C7.2,1.4 6.9,0.6 6.4,0.6 Z" id="Path"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
@@ -62,10 +62,25 @@ export class Button extends Component<JSX.HTMLAttributes & Props, State> {
|
||||
this.props.onFocus!(e);
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, state: State) {
|
||||
const { children } = props;
|
||||
render(props: RenderableProps<JSX.HTMLAttributes & Props>, state: State) {
|
||||
const { children, className } = props;
|
||||
const { isClicked, isFocused } = state;
|
||||
|
||||
let rclassName = b(
|
||||
'button',
|
||||
{ mix: props.mix },
|
||||
{ theme: props.theme, type: props.type, kind: props.kind, clicked: isClicked, focused: isFocused }
|
||||
);
|
||||
if (className) {
|
||||
rclassName +=
|
||||
' ' +
|
||||
b(
|
||||
className,
|
||||
{},
|
||||
{ theme: props.theme, type: props.type, kind: props.kind, clicked: isClicked, focused: isFocused }
|
||||
);
|
||||
}
|
||||
|
||||
const localProps = { ...props };
|
||||
delete localProps.children;
|
||||
delete localProps.mix;
|
||||
@@ -73,11 +88,7 @@ export class Button extends Component<JSX.HTMLAttributes & Props, State> {
|
||||
return (
|
||||
<button
|
||||
{...localProps}
|
||||
className={b(
|
||||
'button',
|
||||
{ mix: props.mix },
|
||||
{ theme: props.theme, type: props.type, kind: props.kind, clicked: isClicked, focused: isFocused }
|
||||
)}
|
||||
className={rclassName}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
|
||||
@@ -320,13 +320,19 @@ export class Comment extends Component<Props, State> {
|
||||
return !this.props.user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines whether current client is logged in via `Anonymous provider`
|
||||
*/
|
||||
isAnonymous(): boolean {
|
||||
return this.props.user! && this.props.user!.id.substr(0, 10) === 'anonymous_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines whether comment made by logged in user
|
||||
*/
|
||||
isCurrentUser(): boolean {
|
||||
if (this.isGuest()) {
|
||||
return false;
|
||||
}
|
||||
if (this.isGuest()) return false;
|
||||
|
||||
return this.props.data.user.id === this.props.user!.id;
|
||||
}
|
||||
|
||||
@@ -340,6 +346,7 @@ export class Comment extends Component<Props, State> {
|
||||
if (this.isCurrentUser()) return "Can't vote for your own comment";
|
||||
if (StaticStore.config.positive_score && this.props.data.score < 1) return 'Only positive score allowed';
|
||||
if (this.isGuest()) return 'Sign in to vote';
|
||||
if (this.isAnonymous()) return "Anonymous users can't vote";
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -352,6 +359,7 @@ export class Comment extends Component<Props, State> {
|
||||
if (this.props.data.delete) return "Can't vote for deleted comment";
|
||||
if (this.isCurrentUser()) return "Can't vote for your own comment";
|
||||
if (this.isGuest()) return 'Sign in to vote';
|
||||
if (this.isAnonymous()) return "Anonymous users can't vote";
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
outline-width: 0;
|
||||
display: none;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 5px);
|
||||
width: 170px;
|
||||
left: 0%;
|
||||
transform: translate(-0.5em, 5px);
|
||||
min-width: 170px;
|
||||
max-width: 260px;
|
||||
border: 2px solid #259c9a;
|
||||
border-radius: 3px;
|
||||
padding: 0 0 5px;
|
||||
|
||||
@@ -10,6 +10,5 @@
|
||||
&_separator {
|
||||
border-bottom: 1px solid #259c9a;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.dropdown__items {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.dropdown__items:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Theme } from '@app/common/types';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
titleClass?: string;
|
||||
heading?: string;
|
||||
isActive?: boolean;
|
||||
onTitleClick?: () => void;
|
||||
@@ -76,7 +77,7 @@ export default class Dropdown extends Component<Props, State> {
|
||||
}
|
||||
|
||||
render(props: RenderableProps<Props>, { isActive }: State) {
|
||||
const { title, heading, children, mix } = props;
|
||||
const { title, titleClass, heading, children, mix } = props;
|
||||
|
||||
return (
|
||||
<div className={b('dropdown', { mix }, { theme: props.theme, active: isActive })} ref={r => (this.rootNode = r)}>
|
||||
@@ -87,6 +88,7 @@ export default class Dropdown extends Component<Props, State> {
|
||||
type="button"
|
||||
onClick={() => this.onTitleClick()}
|
||||
theme="light"
|
||||
className={titleClass}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Tree,
|
||||
Sorting,
|
||||
Theme,
|
||||
Provider,
|
||||
AuthProvider,
|
||||
BlockTTL,
|
||||
} from '@app/common/types';
|
||||
import {
|
||||
@@ -61,7 +61,7 @@ interface Props {
|
||||
fetchComments(sort: Sorting): Promise<Tree>;
|
||||
fetchUser(): Promise<User | null>;
|
||||
fetchBlockedUsers(): Promise<BlockedUser[]>;
|
||||
logIn(): Promise<User | null>;
|
||||
logIn(p: AuthProvider): Promise<User | null>;
|
||||
logOut(): Promise<void>;
|
||||
setTheme: (theme: Theme) => void;
|
||||
setBlockedVisible: (value: boolean) => boolean;
|
||||
@@ -289,7 +289,7 @@ const mapDispatchToProps = (dispatch: StoreDispatch) => {
|
||||
fetchUser: () => dispatch(fetchUser()),
|
||||
fetchBlockedUsers: () => dispatch(fetchBlockedUsers()),
|
||||
setBlockedVisible: (value: boolean) => dispatch(setBlockedVisibleState(value)),
|
||||
logIn: (provider: Provider) => dispatch(logIn(provider)),
|
||||
logIn: (provider: AuthProvider) => dispatch(logIn(provider)),
|
||||
logOut: () => dispatch(logout()),
|
||||
setTheme: (theme: Theme) => dispatch(setTheme(theme)),
|
||||
enableComments: () => dispatch(setCommentsReadOnlyState(false)),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import api from '@app/common/api';
|
||||
import { User, BlockedUser, Provider, BlockTTL } from '@app/common/types';
|
||||
import { User, BlockedUser, AuthProvider, BlockTTL } from '@app/common/types';
|
||||
import { ttlToTime } from '@app/utils/ttl-to-time';
|
||||
|
||||
import { StoreAction } from '../index';
|
||||
@@ -16,7 +16,7 @@ export const fetchUser = (): StoreAction<Promise<User | null>> => async dispatch
|
||||
return user;
|
||||
};
|
||||
|
||||
export const logIn = (provider: Provider): StoreAction<Promise<User | null>> => async dispatch => {
|
||||
export const logIn = (provider: AuthProvider): StoreAction<Promise<User | null>> => async dispatch => {
|
||||
const user = await api.logIn(provider);
|
||||
dispatch({
|
||||
type: USER_SET,
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('user', () => {
|
||||
);
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn();
|
||||
await logIn('google')(dispatch, getState, undefined);
|
||||
await logIn({ name: 'google' })(dispatch, getState, undefined);
|
||||
expect(dispatch).toBeCalledWith({
|
||||
type: USER_SET,
|
||||
user: {
|
||||
@@ -70,7 +70,7 @@ describe('user', () => {
|
||||
);
|
||||
const dispatch = jest.fn();
|
||||
const getState = jest.fn();
|
||||
await logIn('google')(dispatch, getState, undefined).catch(() => {});
|
||||
await logIn({ name: 'google' })(dispatch, getState, undefined).catch(() => {});
|
||||
expect(dispatch).not.toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
Generated
+533
-222
File diff suppressed because it is too large
Load Diff
+10
-10
@@ -30,24 +30,24 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.3.4",
|
||||
"@babel/core": "^7.4.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@babel/preset-env": "^7.3.4",
|
||||
"@babel/preset-env": "^7.4.2",
|
||||
"@babel/preset-react": "^7.0.0",
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/jest": "^24.0.11",
|
||||
"@types/node": "^11.11.3",
|
||||
"@typescript-eslint/eslint-plugin": "^1.4.2",
|
||||
"@typescript-eslint/parser": "^1.4.2",
|
||||
"@types/node": "^11.11.6",
|
||||
"@typescript-eslint/eslint-plugin": "^1.5.0",
|
||||
"@typescript-eslint/parser": "^1.5.0",
|
||||
"autoprefixer": "^9.5.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^24.5.0",
|
||||
"babel-loader": "^8.0.5",
|
||||
"clean-webpack-plugin": "^1.0.1",
|
||||
"copy-webpack-plugin": "^5.0.1",
|
||||
"clean-webpack-plugin": "^2.0.1",
|
||||
"copy-webpack-plugin": "^5.0.2",
|
||||
"css-loader": "^2.1.1",
|
||||
"eslint": "^5.15.2",
|
||||
"eslint": "^5.15.3",
|
||||
"eslint-config-prettier": "^4.1.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.2.1",
|
||||
"eslint-plugin-prettier": "^3.0.1",
|
||||
@@ -72,7 +72,7 @@
|
||||
"style-loader": "^0.23.1",
|
||||
"ts-jest": "^24.0.0",
|
||||
"ts-loader": "^5.3.3",
|
||||
"typescript": "^3.3.3333",
|
||||
"typescript": "^3.3.4000",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-bundle-analyzer": "^3.1.0",
|
||||
"webpack-cli": "^3.3.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"bem-react-helper": "^1.1.2",
|
||||
"core-js": "^2.6.5",
|
||||
"core-js": "^3.0.0",
|
||||
"focus-visible": "^4.1.5",
|
||||
"preact": "^8.4.2",
|
||||
"preact-redux": "^2.0.3",
|
||||
|
||||
@@ -91,7 +91,7 @@ module.exports = () => ({
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new Clean(publicFolder),
|
||||
new Clean(),
|
||||
new Define({
|
||||
'process.env.NODE_ENV': JSON.stringify(env),
|
||||
'process.env.REMARK_NODE': JSON.stringify(NODE_ID),
|
||||
|
||||
Reference in New Issue
Block a user