Small refactoring (#609)

* Move visibility param from redux store to local state

* use func for parsing location.search

* Remove bad wrapper

* we already have wrapper with NODE_ID on page and we shouldn't another one with the same id on page
* move common part of markup to level up

* Tinny refac of setSorting

* add dummy types for pollyfils

also, a bit rewrited way to resolve imports

* Move sort flag to local store

Because we don't need to share this param between diffrent parts of interface

* Add action creators

* move action to action creators

* Remove unused functions

* Merge in conditions in one

* One way for export API methods

* add default tags

* Rewrited changing read only mode

* removed unused function

* set sort changes

* always send sort from store
* rollback if sort don't  work

* constanst

* add typing
* shorten export
* remove double define of host
This commit is contained in:
Pavel Mineev
2020-03-07 12:25:18 -06:00
committed by GitHub
parent 30f3ffe517
commit 85c71716f9
32 changed files with 301 additions and 484 deletions
+2
View File
@@ -0,0 +1,2 @@
declare module 'intersection-observer';
declare module 'whatwg-fetch';
+11
View File
@@ -0,0 +1,11 @@
import { Theme } from '@app/common/types';
import { CommentsConfig } from '@app/common/config-types';
declare global {
interface Window {
remark_config: CommentsConfig;
REMARK42: {
changeTheme(theme: Theme): void;
};
}
}
-28
View File
@@ -292,31 +292,3 @@ export const emailConfirmationForSubscribe = (token: string) =>
* Decline current subscription to updates
*/
export const unsubscribeFromEmailUpdates = () => fetcher.delete({ url: `/email`, withCredentials: true });
export default {
logIn,
logOut,
getConfig,
getPostComments,
getCommentsCount,
getComment,
getUserComments,
putCommentVote,
addComment,
updateComment,
removeMyComment,
getUser,
getPreview,
pinComment,
unpinComment,
setVerifyStatus: setVerifiedStatus,
removeVerifyStatus: removeVerifiedStatus,
removeComment,
blockUser,
unblockUser,
getBlocked,
disableComments,
enableComments,
uploadImage,
};
+2 -4
View File
@@ -1,7 +1,5 @@
export const BASE_URL: string =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((window as any).remark_config && (window as any).remark_config.host) || process.env.REMARK_URL!;
export const NODE_ID: string = process.env.REMARK_NODE!;
export const BASE_URL = (window.remark_config && window.remark_config.host) || process.env.REMARK_URL!;
export const NODE_ID = process.env.REMARK_NODE!;
export const API_BASE = '/api/v1';
export const COMMENT_NODE_CLASSNAME_PREFIX = 'remark42__comment-';
export const COUNTER_NODE_CLASSNAME = 'remark42__counter';
+3 -7
View File
@@ -1,10 +1,6 @@
import { Sorting, AuthProvider, Theme } from './types';
import * as configConstant from './constants.config';
export const BASE_URL = configConstant.BASE_URL;
export const API_BASE = configConstant.API_BASE;
export const NODE_ID = configConstant.NODE_ID;
export const COMMENT_NODE_CLASSNAME_PREFIX = configConstant.COMMENT_NODE_CLASSNAME_PREFIX;
export { BASE_URL, API_BASE, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from './constants.config';
export const LAST_COMMENTS_NODE_CLASSNAME = 'remark42__last-comments';
export const MAX_SHOWN_ROOT_COMMENTS = 10;
@@ -28,8 +24,8 @@ export const LS_COLLAPSE_KEY = '__remarkCollapsed';
/** locastorage key for hidden users */
export const LS_HIDDEN_USERS_KEY = '__remarkHiddenUsers';
/** cookie key under which sort preference resides */
export const COOKIE_SORT_KEY = 'remarkSort';
/** localstorage key under which sort preference resides */
export const LS_SORT_KEY = '__remarkSort';
export const THEMES: Theme[] = ['light', 'dark'];
+12 -13
View File
@@ -5,27 +5,27 @@ import '@webcomponents/custom-elements';
import './closest-polyfill';
export default async function loadPolyfills() {
const fillCoreJs = async () => {
function fillCoreJs() {
if (
'startsWith' in String.prototype &&
'endsWith' in String.prototype &&
'includes' in Array.prototype &&
'assign' in Object &&
'keys' in Object
)
) {
return;
}
await import(/* webpackChunkName: "core-js" */ 'core-js').then();
return;
};
return import(/* webpackChunkName: "core-js" */ 'core-js');
}
const fillFetch = async () => {
function fillFetch() {
if ('fetch' in window) return;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch' as any).then();
};
const fillIntersectionObserver = async () => {
return import(/* webpackChunkName: "whatwg-fetch" */ 'whatwg-fetch');
}
function fillIntersectionObserver() {
if (
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
@@ -34,9 +34,8 @@ export default async function loadPolyfills() {
return;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer' as any).then();
};
return import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer');
}
await Promise.all([fillCoreJs(), fillFetch(), fillIntersectionObserver()]);
return;
+2 -9
View File
@@ -1,5 +1,6 @@
import { Theme } from './types';
import { THEMES, MAX_SHOWN_ROOT_COMMENTS } from './constants';
import parseQuery from '@app/utils/parseQuery';
export interface QuerySettingsType {
site_id?: string;
@@ -11,15 +12,7 @@ export interface QuerySettingsType {
token?: string;
}
export const querySettings: Partial<QuerySettingsType> =
window.location.search
.substr(1)
.split('&')
.reduce<{ [key: string]: string }>((acc, param) => {
const pair = param.split('=');
acc[pair[0]] = decodeURIComponent(pair[1]);
return acc;
}, {}) || {};
export const querySettings: Partial<QuerySettingsType> = parseQuery();
if (querySettings.max_shown_comments) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+3 -16
View File
@@ -1,23 +1,10 @@
import { UserInfo } from './types';
import parseQuery from '@app/utils/parseQuery';
export const userInfo: Partial<UserInfo> =
window.location.search
.substr(1)
.split('&')
.reduce<{ [key: string]: string }>((acc, param) => {
const pair = param.split('=');
acc[pair[0]] = decodeURIComponent(pair[1]);
return acc;
}, {}) || {};
export const userInfo: Partial<UserInfo> = parseQuery();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (((userInfo.isDefaultPicture as any) as string) !== '1') {
userInfo.isDefaultPicture = false;
} else {
userInfo.isDefaultPicture = true;
}
export const isDefaultPicture = ((userInfo.isDefaultPicture as any) as string) !== '1';
export const id = userInfo.id;
export const name = userInfo.name;
export const isDefaultPicture = userInfo.isDefaultPicture;
export const picture = userInfo.picture;
@@ -32,8 +32,7 @@ interface PropsWithoutIntl {
onSortChange(s: Sorting): Promise<void>;
onSignIn(p: AuthProvider): Promise<User | null>;
onSignOut(): Promise<void>;
onCommentsEnable(): Promise<boolean>;
onCommentsDisable(): Promise<boolean>;
onCommentsChangeReadOnlyMode(readOnly: boolean): Promise<void>;
onBlockedUsersShow(): void;
onBlockedUsersHide(): void;
}
@@ -76,7 +75,6 @@ export class AuthPanel extends Component<Props, State> {
};
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.onEmailSignIn = this.onEmailSignIn.bind(this);
@@ -108,9 +106,7 @@ export class AuthPanel extends Component<Props, State> {
}
onSortChange(e: Event) {
if (this.props.onSortChange) {
this.props.onSortChange((e.target! as HTMLOptionElement).value as Sorting);
}
this.props.onSortChange((e.target! as HTMLOptionElement).value as Sorting);
}
onSortFocus = () => {
@@ -131,13 +127,9 @@ export class AuthPanel extends Component<Props, State> {
this.setState({ isBlockedVisible: !this.state.isBlockedVisible });
}
toggleCommentsAvailability() {
if (this.props.isCommentsDisabled) {
this.props.onCommentsEnable && this.props.onCommentsEnable();
} else {
this.props.onCommentsDisable && this.props.onCommentsDisable();
}
}
toggleCommentsAvailability = () => {
this.props.onCommentsChangeReadOnlyMode(!this.props.isCommentsDisabled);
};
toggleUserInfoVisibility() {
const user = this.props.user;
@@ -368,7 +360,7 @@ export class AuthPanel extends Component<Props, State> {
<Button
kind="link"
mix="auth-panel__admin-action"
{...getHandleClickProps(() => this.toggleCommentsAvailability())}
{...getHandleClickProps(this.toggleCommentsAvailability)}
role="link"
>
{isCommentsDisabled ? (
@@ -384,6 +376,7 @@ export class AuthPanel extends Component<Props, State> {
const { sort } = this.props;
const { sortSelectFocused } = this.state;
const sortArray = getSortArray(sort, this.props.intl);
return (
<span className="auth-panel__sort">
<FormattedMessage id="commentSort.sort-by" defaultMessage="Sort by" />{' '}
+1 -1
View File
@@ -252,7 +252,7 @@ class Comment extends Component<Props, State> {
: intl.formatMessage(messages.unverifyUser, { userName });
if (confirm(promptMessage)) {
this.props.setVerifyStatus!(userId, value);
this.props.setVerifiedStatus!(userId, value);
}
};
@@ -69,7 +69,7 @@ export const boundActions = bindActions({
blockUser,
unblockUser,
hideUser,
setVerifyStatus: setVerifiedStatus,
setVerifiedStatus,
});
export const ConnectedComment: FunctionComponent<Omit<Props, keyof (ProvidedProps & typeof bindActions)>> = props => {
+124 -123
View File
@@ -1,16 +1,17 @@
/** @jsx createElement */
import { createElement, Component, FunctionComponent } from 'preact';
import { createElement, Component, FunctionComponent, Fragment } from 'preact';
import { useSelector } from 'react-redux';
import b from 'bem-react-helper';
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
import { User, Sorting, AuthProvider } from '@app/common/types';
import {
NODE_ID,
COMMENT_NODE_CLASSNAME_PREFIX,
MAX_SHOWN_ROOT_COMMENTS,
THEMES,
IS_MOBILE,
LS_SORT_KEY,
DEFAULT_SORT,
} from '@app/common/constants';
import { maxShownComments } from '@app/common/settings';
@@ -23,14 +24,12 @@ import {
blockUser,
unblockUser,
fetchBlockedUsers,
setSettingsVisibility,
hideUser,
unhideUser,
} from '@app/store/user/actions';
import { fetchComments } from '@app/store/comments/actions';
import { setCommentsReadOnlyState } from '@app/store/post_info/actions';
import { setTheme } from '@app/store/theme/actions';
import { setSort } from '@app/store/sort/actions';
import { addComment, updateComment } from '@app/store/comments/actions';
import { AuthPanel } from '@app/components/auth-panel';
@@ -45,11 +44,10 @@ import { isUserAnonymous } from '@app/utils/isUserAnonymous';
import { bindActions } from '@app/utils/actionBinder';
import postMessage from '@app/utils/postMessage';
import { useActions } from '@app/hooks/useAction';
import * as localStorage from '@app/common/local-storage';
const mapStateToProps = (state: StoreState) => ({
user: state.user,
sort: state.sort,
isSettingsVisible: state.isSettingsVisible,
topComments: state.topComments,
pinnedComments: state.pinnedComments.map(id => state.comments[id]).filter(c => !c.hidden),
provider: state.provider,
@@ -65,13 +63,10 @@ const boundActions = bindActions({
fetchComments,
fetchUser,
fetchBlockedUsers,
setSettingsVisibility,
logIn,
logOut: logout,
setTheme,
enableComments: () => setCommentsReadOnlyState(false),
disableComments: () => setCommentsReadOnlyState(true),
changeSort: setSort,
setCommentsReadOnlyState,
blockUser,
unblockUser,
hideUser,
@@ -83,8 +78,10 @@ const boundActions = bindActions({
type Props = ReturnType<typeof mapStateToProps> & typeof boundActions & { intl: IntlShape };
interface State {
isLoaded: boolean;
sort: string;
isUserLoading: boolean;
isCommentsListLoading: boolean;
isSettingsVisible: boolean;
commentsShown: number;
wasSomeoneUnblocked: boolean;
}
@@ -96,30 +93,33 @@ const messages = defineMessages({
},
});
/** main component fr main comments widget */
export class Root extends Component<Props, State> {
constructor(props: Props) {
super(props);
function getInitialSort() {
const sort = localStorage.getItem(LS_SORT_KEY) as Sorting;
this.state = {
isLoaded: false,
isCommentsListLoading: false,
commentsShown: maxShownComments,
wasSomeoneUnblocked: false,
};
this.onBlockedUsersShow = this.onBlockedUsersShow.bind(this);
this.onBlockedUsersHide = this.onBlockedUsersHide.bind(this);
this.onUnblockSomeone = this.onUnblockSomeone.bind(this);
this.showMore = this.showMore.bind(this);
if (sort) {
return sort;
}
async componentWillMount() {
Promise.all([this.props.fetchUser(), this.props.fetchComments(this.props.sort)]).finally(() => {
this.setState({
isLoaded: true,
});
return DEFAULT_SORT;
}
/** main component fr main comments widget */
export class Root extends Component<Props, State> {
state = {
sort: getInitialSort(),
isUserLoading: true,
isCommentsListLoading: true,
commentsShown: maxShownComments,
wasSomeoneUnblocked: false,
isSettingsVisible: false,
};
componentWillMount() {
const userloading = this.props.fetchUser().finally(() => this.setState({ isUserLoading: false }));
Promise.all([userloading, this.fetchComments()]).finally(() => {
postMessage({ remarkIframeHeight: document.body.offsetHeight });
this.setState({ isCommentsListLoading: false });
setTimeout(this.checkUrlHash);
window.addEventListener('hashchange', this.checkUrlHash);
});
@@ -127,22 +127,38 @@ export class Root extends Component<Props, State> {
window.addEventListener('message', this.onMessage.bind(this));
}
logIn = async (p: AuthProvider): Promise<User | null> => {
const user = await this.props.logIn(p);
await this.props.fetchComments(this.props.sort);
fetchComments() {
return this.props.fetchComments(this.state.sort);
}
changeSort = async (sort: Sorting) => {
if (sort === this.state.sort) return;
const prevSort = this.state.sort;
this.setState({ isCommentsListLoading: true, sort });
try {
await this.fetchComments();
localStorage.setItem(LS_SORT_KEY, sort);
this.setState({ isCommentsListLoading: false });
} catch (e) {
this.setState({ sort: prevSort, isCommentsListLoading: false });
}
};
logIn = async (provider: AuthProvider): Promise<User | null> => {
const user = await this.props.logIn(provider);
await this.fetchComments();
return user;
};
logOut = async (): Promise<void> => {
await this.props.logOut();
await this.props.fetchComments(this.props.sort);
await this.fetchComments();
};
checkUrlHash(
e: Event & {
newURL?: string;
}
) {
checkUrlHash(e: Event & { newURL?: string }) {
const hash = e ? `#${e.newURL!.split('#')[1]}` : window.location.hash;
if (hash.indexOf(`#${COMMENT_NODE_CLASSNAME_PREFIX}`) === 0) {
@@ -169,40 +185,33 @@ export class Root extends Component<Props, State> {
}
}
async onBlockedUsersShow() {
onBlockedUsersShow = async () => {
if (this.props.user && this.props.user.admin) {
await this.props.fetchBlockedUsers();
}
this.props.setSettingsVisibility(true);
}
this.setState({ isSettingsVisible: true });
};
async onBlockedUsersHide() {
onBlockedUsersHide = async () => {
// if someone was unblocked let's reload comments
if (this.state.wasSomeoneUnblocked) {
this.props.fetchComments(this.props.sort);
this.fetchComments();
}
this.props.setSettingsVisibility(false);
this.setState({
wasSomeoneUnblocked: false,
isSettingsVisible: false,
});
}
};
async changeSort(sort: Sorting) {
if (sort === this.props.sort) return;
this.setState({ isCommentsListLoading: true });
await this.props.changeSort(sort).catch(() => {});
this.setState({ isCommentsListLoading: false });
}
onUnblockSomeone() {
onUnblockSomeone = () => {
this.setState({ wasSomeoneUnblocked: true });
}
};
showMore() {
showMore = () => {
this.setState({
commentsShown: this.state.commentsShown + MAX_SHOWN_ROOT_COMMENTS,
});
}
};
/**
* Defines whether current client is logged in via `Anonymous provider`
@@ -211,44 +220,48 @@ export class Root extends Component<Props, State> {
return isUserAnonymous(this.props.user);
}
render(props: Props, { isLoaded, isCommentsListLoading, commentsShown }: State) {
if (!isLoaded) {
return (
<div id={NODE_ID}>
<div className={b('root', {}, { theme: props.theme })}>
<Preloader mix="root__preloader" />
</div>
</div>
);
render(props: Props, { isUserLoading, isCommentsListLoading, commentsShown, isSettingsVisible }: State) {
if (isUserLoading) {
return <Preloader mix="root__preloader" />;
}
const isGuest = !props.user;
const isCommentsDisabled = !!props.info.read_only;
const isCommentsDisabled = props.info.read_only!;
const imageUploadHandler = this.isAnonymous() ? undefined : this.props.uploadImage;
return (
<div id={NODE_ID}>
<div className={b('root', {}, { theme: props.theme })}>
<AuthPanel
theme={this.props.theme}
user={this.props.user}
hiddenUsers={this.props.hiddenUsers}
sort={this.props.sort}
isCommentsDisabled={isCommentsDisabled}
postInfo={this.props.info}
providers={StaticStore.config.auth_providers}
provider={this.props.provider}
onSignIn={this.logIn}
onSignOut={this.logOut}
onBlockedUsersShow={this.onBlockedUsersShow}
onBlockedUsersHide={this.onBlockedUsersHide}
onCommentsEnable={this.props.enableComments}
onCommentsDisable={this.props.disableComments}
onSortChange={this.props.changeSort}
/>
{!this.props.isSettingsVisible && (
<div className="root__main">
<Fragment>
<AuthPanel
theme={this.props.theme}
user={this.props.user}
hiddenUsers={this.props.hiddenUsers}
sort={this.state.sort}
isCommentsDisabled={isCommentsDisabled}
postInfo={this.props.info}
providers={StaticStore.config.auth_providers}
provider={this.props.provider}
onSignIn={this.logIn}
onSignOut={this.logOut}
onBlockedUsersShow={this.onBlockedUsersShow}
onBlockedUsersHide={this.onBlockedUsersHide}
onCommentsChangeReadOnlyMode={this.props.setCommentsReadOnlyState}
onSortChange={this.changeSort}
/>
<div className="root__main">
{isSettingsVisible ? (
<Settings
intl={this.props.intl}
user={this.props.user}
hiddenUsers={this.props.hiddenUsers}
blockedUsers={this.props.blockedUsers}
blockUser={this.props.blockUser}
unblockUser={this.props.unblockUser}
hideUser={this.props.hideUser}
unhideUser={this.props.unhideUser}
onUnblockSomeone={this.onUnblockSomeone}
/>
) : (
<Fragment>
{!isGuest && !isCommentsDisabled && (
<CommentForm
intl={this.props.intl}
@@ -312,40 +325,10 @@ export class Root extends Component<Props, State> {
<Preloader mix="root__preloader" />
</div>
)}
</div>
</Fragment>
)}
{this.props.isSettingsVisible && (
<div className="root__main">
<Settings
intl={this.props.intl}
user={this.props.user}
hiddenUsers={this.props.hiddenUsers}
blockedUsers={this.props.blockedUsers}
blockUser={this.props.blockUser}
unblockUser={this.props.unblockUser}
hideUser={this.props.hideUser}
unhideUser={this.props.unhideUser}
onUnblockSomeone={this.onUnblockSomeone}
/>
</div>
)}
<p className="root__copyright" role="contentinfo">
<FormattedMessage
id="root.powered-by"
defaultMessage="Powered by <a>Remark42</a>"
values={{
a: (title: string) => (
<a class="root__copyright-link" href="https://remark42.com/">
{title}
</a>
),
}}
/>
</p>
</div>
</div>
</Fragment>
);
}
}
@@ -355,5 +338,23 @@ export const ConnectedRoot: FunctionComponent = () => {
const props = useSelector(mapStateToProps);
const actions = useActions(boundActions);
const intl = useIntl();
return <Root {...props} {...actions} intl={intl} />;
return (
<div className={b('root', {}, { theme: props.theme })}>
<Root {...props} {...actions} intl={intl} />
<p className="root__copyright" role="contentinfo">
<FormattedMessage
id="root.powered-by"
defaultMessage="Powered by <a>Remark42</a>"
values={{
a: (title: string) => (
<a class="root__copyright-link" href="https://remark42.com/">
{title}
</a>
),
}}
/>
</p>
</div>
);
};
+23 -28
View File
@@ -1,11 +1,6 @@
/* eslint-disable no-console, @typescript-eslint/no-explicit-any */
declare let remark_config: CommentsConfig;
/* eslint-disable no-console */
import { BASE_URL, NODE_ID, COMMENT_NODE_CLASSNAME_PREFIX } from '@app/common/constants.config';
import { UserInfo, Theme } from '@app/common/types';
import { CommentsConfig } from '@app/common/config-types';
const HOST = remark_config.host || BASE_URL;
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
@@ -28,7 +23,7 @@ function createFrame({
host: string;
query: string;
height?: string;
__colors__?: any;
__colors__?: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}) {
const iframe = document.createElement('iframe');
iframe.src = `${host}/web/iframe.html?${query}`;
@@ -51,8 +46,8 @@ function createFrame({
return iframe;
}
function init(): void {
const node = document.getElementById(remark_config.node || NODE_ID);
function init() {
const node = document.getElementById(window.remark_config.node || NODE_ID);
if (!node) {
console.error("Remark42: Can't find root node.");
@@ -60,29 +55,33 @@ function init(): void {
}
try {
remark_config = remark_config || {};
window.remark_config = window.remark_config || {};
} catch (e) {
console.error('Remark42: Config object is undefined.');
return;
}
if (!remark_config.site_id) {
if (!window.remark_config.site_id) {
console.error('Remark42: Site ID is undefined.');
return;
}
remark_config.url = (remark_config.url || window.location.origin + window.location.pathname).split('#')[0];
window.remark_config.url = (window.remark_config.url || window.location.origin + window.location.pathname).split(
'#'
)[0];
(window as any).REMARK42 = (window as any).REMARK42 || {};
(window as any).REMARK42.changeTheme = changeTheme;
window.REMARK42 = window.REMARK42 || {};
window.REMARK42.changeTheme = changeTheme;
const query = Object.keys(remark_config)
.filter((key: any) => key !== `__colors__`)
.map((key: any) => {
return `${encodeURIComponent(key)}=${encodeURIComponent((remark_config as any)[key])}`;
const query = Object.keys(window.remark_config)
.filter(key => key !== '__colors__')
.map(key => {
return `${encodeURIComponent(key)}=${encodeURIComponent(
window.remark_config[key as keyof typeof window.remark_config]
)}`;
})
.join('&');
const iframe = createFrame({ host: HOST, query, __colors__: remark_config.__colors__ });
const iframe = createFrame({ host: BASE_URL, query, __colors__: window.remark_config.__colors__ });
node.appendChild(iframe);
@@ -195,7 +194,7 @@ function init(): void {
query +
'&page=user-info&' +
`&id=${user.id}&name=${user.name}&picture=${user.picture || ''}&isDefaultPicture=${user.isDefaultPicture || 0}`;
const iframe = createFrame({ host: HOST, query: queryUserInfo, height: '100%' });
const iframe = createFrame({ host: BASE_URL, query: queryUserInfo, height: '100%' });
this.node.appendChild(iframe);
this.iframe = iframe;
this.node.appendChild(this.closeEl);
@@ -284,11 +283,7 @@ function init(): void {
} catch (e) {}
}
function postHashToIframe(
e?: Event & {
newURL: string;
}
): void {
function postHashToIframe(e?: Event & { newURL: string }) {
const hash = e ? `#${e.newURL.split('#')[1]}` : window.location.hash;
if (hash.indexOf(`#${COMMENT_NODE_CLASSNAME_PREFIX}`) === 0) {
@@ -298,17 +293,17 @@ function init(): void {
}
}
function postTitleToIframe(title: string): void {
function postTitleToIframe(title: string) {
iframe.contentWindow!.postMessage(JSON.stringify({ title }), '*');
}
function postClickOutsideToIframe(e: MouseEvent): void {
function postClickOutsideToIframe(e: MouseEvent) {
if (!iframe.contains(e.target as Node)) {
iframe.contentWindow!.postMessage(JSON.stringify({ clickOutside: true }), '*');
}
}
function changeTheme(theme: Theme): void {
function changeTheme(theme: Theme) {
iframe.contentWindow!.postMessage(JSON.stringify({ theme }), '*');
}
}
+8 -18
View File
@@ -24,10 +24,11 @@ import '@app/components/list-comments';
import { NODE_ID, BASE_URL } from '@app/common/constants';
import { StaticStore } from '@app/common/static_store';
import api from '@app/common/api';
import { getConfig } from '@app/common/api';
import { fetchHiddenUsers } from './store/user/actions';
import { restoreProvider } from './store/provider/actions';
import { restoreCollapsedThreads } from './store/thread/actions';
import parseQuery from './utils/parseQuery';
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
@@ -55,29 +56,18 @@ async function init(): Promise<void> {
boundActions.restoreProvider();
boundActions.restoreCollapsedThreads();
const params = window.location.search
.replace(/^\?/, '')
.split('&')
.reduce<{ [key: string]: string }>((memo, value) => {
const vals = value.split('=');
if (vals.length === 2) {
memo[vals[0]] = vals[1];
}
return memo;
}, {});
const params = parseQuery();
const locale = getLocale(params);
const messages = await loadLocale(locale).catch(() => ({}));
StaticStore.config = await api.getConfig();
StaticStore.config = await getConfig();
if (params.page === 'user-info') {
return render(
<IntlProvider locale={locale} messages={messages}>
<div id={NODE_ID}>
<div className="root root_user-info">
<Provider store={reduxStore}>
<UserInfo />
</Provider>
</div>
<div className="root root_user-info">
<Provider store={reduxStore}>
<UserInfo />
</Provider>
</div>
</IntlProvider>,
node
-2
View File
@@ -1,6 +1,5 @@
import { COMMENTS_ACTIONS } from './comments/types';
import { POST_INFO_ACTIONS } from './post_info/types';
import { SORT_ACTIONS } from './sort/types';
import { THEME_ACTIONS } from './theme/types';
import { THREAD_ACTIONS } from './thread/types';
import { USER_ACTIONS } from './user/types';
@@ -11,7 +10,6 @@ import { PROVIDER_ACTIONS } from './provider/types';
export type ACTIONS =
| COMMENTS_ACTIONS
| POST_INFO_ACTIONS
| SORT_ACTIONS
| THEME_ACTIONS
| THREAD_ACTIONS
| USER_ACTIONS
+18 -19
View File
@@ -1,10 +1,10 @@
import api from '@app/common/api';
import { Tree, Comment, Sorting, CommentMode, Node } from '@app/common/types';
import * as api from '@app/common/api';
import { Tree, Comment, CommentMode, Node, Sorting } from '@app/common/types';
import { StoreAction, StoreState } from '../index';
import { POST_INFO_SET } from '../post_info/types';
import { setPostInfo } from '../post_info/actions';
import { filterTree } from './utils';
import { COMMENTS_SET, COMMENT_MODE_SET, COMMENTS_APPEND, COMMENTS_EDIT } from './types';
import { COMMENTS_SET, COMMENT_MODE_SET, COMMENTS_APPEND, COMMENTS_EDIT, COMMENT_MODE_SET_ACTION } from './types';
/** sets comments, and put pinned comments in cache */
export const setComments = (comments: Node[]): StoreAction<void> => dispatch => {
@@ -68,15 +68,17 @@ export const removeComment = (id: Comment['id']): StoreAction<Promise<void>> =>
/** fetches comments from server */
export const fetchComments = (sort: Sorting): StoreAction<Promise<Tree>> => async (dispatch, getState) => {
const { hiddenUsers } = getState();
const hiddenUsersIds = Object.keys(hiddenUsers);
const data = await api.getPostComments(sort);
const hiddenUsersIds = Object.keys(getState().hiddenUsers);
if (hiddenUsersIds.length > 0)
if (hiddenUsersIds.length > 0) {
data.comments = filterTree(data.comments, node => hiddenUsersIds.indexOf(node.comment.user.id) === -1);
}
dispatch(setComments(data.comments));
dispatch({
type: POST_INFO_SET,
info: data.info,
});
dispatch(setPostInfo(data.info));
return data;
};
@@ -85,16 +87,13 @@ export const setCommentMode = (mode: StoreState['activeComment']): StoreAction<v
if (mode !== null && mode.state === CommentMode.None) {
mode = null;
}
dispatch({
type: COMMENT_MODE_SET,
mode,
});
dispatch(unsetCommentMode(mode));
};
/** unsets comment mode */
export const unsetCommentMode = (): StoreAction<void> => dispatch => {
dispatch({
export function unsetCommentMode(mode: StoreState['activeComment'] = null) {
return {
type: COMMENT_MODE_SET,
mode: null,
});
};
mode,
} as COMMENT_MODE_SET_ACTION;
}
+2 -4
View File
@@ -2,11 +2,9 @@ import { Comment, CommentMode } from '@app/common/types';
import { StoreState } from '../index';
export const getCommentMode = (id: Comment['id']) => (state: StoreState): CommentMode => {
if (state.activeComment === null) {
return CommentMode.None;
}
if (state.activeComment.id !== id) {
if (state.activeComment === null || state.activeComment.id !== id) {
return CommentMode.None;
}
return state.activeComment.state;
};
+14 -30
View File
@@ -1,40 +1,24 @@
import { PostInfo } from '@app/common/types';
import { StoreAction } from '../index';
import { POST_INFO_SET } from './types';
import api from '@app/common/api';
import { POST_INFO_SET, POST_INFO_SET_ACTION } from './types';
import { disableComments, enableComments } from '@app/common/api';
import { unsetCommentMode } from '../comments/actions';
export const setPostInfo = (info: PostInfo): StoreAction<void> => dispatch =>
dispatch({
export function setPostInfo(info: PostInfo) {
return {
type: POST_INFO_SET,
info,
});
} as POST_INFO_SET_ACTION;
}
/** set state of post: readonly or not */
export const setCommentsReadOnlyState = (state: boolean): StoreAction<Promise<boolean>> => async (
dispatch,
getState
) => {
await (!state ? api.enableComments() : api.disableComments());
const storeState = getState();
dispatch(unsetCommentMode());
dispatch({
type: POST_INFO_SET,
info: { ...storeState.info, read_only: state },
});
return state;
};
export function setCommentsReadOnlyState(read_only: boolean): StoreAction<Promise<void>> {
return async (dispatch, getState) => {
const { info } = getState();
/** toggles state of post: readonly or not */
export const toggleCommentsReadOnlyState = (): StoreAction<Promise<boolean>> => async (dispatch, getState) => {
const storeState = getState();
const state = !storeState.info.read_only!;
await (state ? api.enableComments() : api.disableComments());
dispatch(unsetCommentMode());
dispatch({
type: POST_INFO_SET,
info: { ...storeState.info, read_only: !state },
});
return !state;
};
await (read_only ? disableComments() : enableComments());
dispatch(unsetCommentMode());
dispatch(setPostInfo({ ...info, read_only }));
};
}
+1 -8
View File
@@ -7,11 +7,4 @@ export interface POST_INFO_SET_ACTION {
info: PostInfo;
}
export const POST_INFO_SET_READONLY = 'COMMENTS/SET_READONLY';
export interface POST_INFO_SET_READONLY_ACTION {
type: typeof POST_INFO_SET_READONLY;
readonly: boolean;
}
export type POST_INFO_ACTIONS = POST_INFO_SET_ACTION | POST_INFO_SET_READONLY_ACTION;
export type POST_INFO_ACTIONS = POST_INFO_SET_ACTION;
-2
View File
@@ -1,6 +1,5 @@
import comments from './comments/reducers';
import postinfo from './post_info/reducers';
import sort from './sort/reducers';
import theme from './theme/reducers';
import user from './user/reducers';
import userInfo from './user-info/reducers';
@@ -11,7 +10,6 @@ import provider from './provider/reducers';
export default {
...comments,
...postinfo,
...sort,
...theme,
...user,
...userInfo,
-40
View File
@@ -1,40 +0,0 @@
import { Sorting } from '@app/common/types';
import { COOKIE_SORT_KEY } from '@app/common/constants';
import { setCookie } from '@app/common/cookies';
import { StoreAction } from '../index';
import { fetchComments } from '../comments/actions';
import { SORT_SET, SORT_SET_ACTION } from './types';
function setSortCookie(sort: Sorting) {
try {
setCookie(COOKIE_SORT_KEY, sort, { expires: 60 * 60 * 24 * 365, path: '/' }); // save sorting for a year
} catch {
// can't save; ignore it
}
}
export const setSort = (sort: Sorting): StoreAction<Promise<void>> => async (dispatch, getState) => {
const originalSort = getState().sort;
setSortCookie(sort);
try {
const action: SORT_SET_ACTION = {
type: SORT_SET,
sort,
};
await dispatch(action);
await dispatch(fetchComments(sort));
} catch {
// restore sort in case of error, probably network error
const action: SORT_SET_ACTION = {
type: SORT_SET,
sort: originalSort,
};
setSortCookie(originalSort);
await dispatch(action);
}
};
-25
View File
@@ -1,25 +0,0 @@
import { Sorting } from '@app/common/types';
import { COOKIE_SORT_KEY, DEFAULT_SORT } from '@app/common/constants';
import { getCookie } from '@app/common/cookies';
import { SORT_SET, SORT_SET_ACTION } from './types';
const getDefaultSort = (): Sorting => {
try {
return (getCookie(COOKIE_SORT_KEY) as Sorting) || DEFAULT_SORT;
} catch (e) {
return DEFAULT_SORT;
}
};
export const sort = (state: Sorting = getDefaultSort(), action: SORT_SET_ACTION): Sorting => {
switch (action.type) {
case SORT_SET: {
return action.sort;
}
default:
return state;
}
};
export default { sort };
-10
View File
@@ -1,10 +0,0 @@
import { Sorting } from '@app/common/types';
export const SORT_SET = 'SORT/SET';
export interface SORT_SET_ACTION {
type: typeof SORT_SET;
sort: Sorting;
}
export type SORT_ACTIONS = SORT_SET_ACTION;
+3 -14
View File
@@ -1,27 +1,16 @@
import api from '@app/common/api';
import { Comment, User } from '@app/common/types';
import { getUserComments } from '@app/common/api';
import { Comment } from '@app/common/types';
import { userInfo } from '@app/common/user-info-settings';
import { StoreAction } from '../index';
import { USER_INFO_SET } from './types';
export const getUserComments = (userId: User['id']): StoreAction<Comment[] | null> => (_dispatch, getState) =>
getState().userComments![userId] || null;
export const getComments = (id: User['id']): StoreAction<Comment[] | null> => (_dispatch, getState) => {
const comments = getState().userComments![id];
if (comments) {
return comments;
}
return null;
};
export const fetchInfo = (): StoreAction<Promise<Comment[] | null>> => async dispatch => {
if (!userInfo.id) {
return null;
}
// TODO: limit
const info = await api.getUserComments(userInfo.id, 10);
const info = await getUserComments(userInfo.id, 10);
dispatch({
type: USER_INFO_SET,
id: userInfo.id,
+16 -24
View File
@@ -1,4 +1,4 @@
import api from '@app/common/api';
import * as api from '@app/common/api';
import { User, BlockedUser, AuthProvider, BlockTTL } from '@app/common/types';
import { ttlToTime } from '@app/utils/ttl-to-time';
@@ -12,7 +12,7 @@ import {
USER_HIDE,
USER_UNHIDE,
USER_SUBSCRIPTION_SET,
SETTINGS_VISIBLE_SET,
USER_SET_ACTION,
} from './types';
import { unsetCommentMode } from '../comments/actions';
import { IS_STORAGE_AVAILABLE, LS_HIDDEN_USERS_KEY } from '@app/common/constants';
@@ -20,32 +20,32 @@ import { getItem } from '@app/common/local-storage';
import { updateProvider } from '../provider/actions';
import { COMMENTS_PATCH } from '../comments/types';
export const fetchUser = (): StoreAction<Promise<User | null>> => async dispatch => {
const user = await api.getUser();
dispatch({
function setUser(user: User | null = null) {
return {
type: USER_SET,
user,
});
} as USER_SET_ACTION;
}
export const fetchUser = (): StoreAction<Promise<User | null>> => async dispatch => {
const user = await api.getUser();
dispatch(setUser(user));
return user;
};
export const logIn = (provider: AuthProvider): StoreAction<Promise<User | null>> => async dispatch => {
const user = await api.logIn(provider);
dispatch(updateProvider({ name: provider.name }));
dispatch({
type: USER_SET,
user,
});
dispatch(setUser(user));
return user;
};
export const logout = (): StoreAction<Promise<void>> => async dispatch => {
await api.logOut();
dispatch(unsetCommentMode());
dispatch({
type: USER_SET,
user: null,
});
dispatch(setUser());
};
export const fetchBlockedUsers = (): StoreAction<Promise<BlockedUser[]>> => async dispatch => {
@@ -134,9 +134,9 @@ export const setVerifiedStatus = (id: User['id'], status: boolean): StoreAction<
getState
) => {
if (status) {
await api.setVerifyStatus(id);
await api.setVerifiedStatus(id);
} else {
await api.removeVerifyStatus(id);
await api.removeVerifiedStatus(id);
}
const comments = Object.values(getState().comments).filter(c => c.user.id === id);
if (!comments.length) return;
@@ -149,14 +149,6 @@ export const setVerifiedStatus = (id: User['id'], status: boolean): StoreAction<
});
};
export const setSettingsVisibility = (state: boolean): StoreAction<boolean> => dispatch => {
dispatch({
type: SETTINGS_VISIBLE_SET,
state,
});
return state;
};
export const setUserSubscribed = (isSubscribed: boolean) => ({
type: USER_SUBSCRIPTION_SET,
payload: isSubscribed,
+1 -1
View File
@@ -1,4 +1,4 @@
import api from '@app/common/api';
import * as api from '@app/common/api';
import { User } from '@app/common/types';
import { fetchUser, logIn, logout } from './actions';
+1 -13
View File
@@ -5,8 +5,6 @@ import {
USER_BAN,
USER_UNBAN,
USER_ACTIONS,
SETTINGS_VISIBLE_SET_ACTION,
SETTINGS_VISIBLE_SET,
USER_BANLIST_SET,
USER_HIDELIST_SET,
USER_HIDE,
@@ -76,14 +74,4 @@ export const hiddenUsers = (state: { [id: string]: User } = {}, action: USER_ACT
}
};
export const isSettingsVisible = (state: boolean = false, action: SETTINGS_VISIBLE_SET_ACTION): boolean => {
switch (action.type) {
case SETTINGS_VISIBLE_SET: {
return action.state;
}
default:
return state;
}
};
export default { user, bannedUsers, hiddenUsers, isSettingsVisible };
export default { user, bannedUsers, hiddenUsers };
-8
View File
@@ -49,13 +49,6 @@ export interface USER_UNHIDE_ACTION {
id: User['id'];
}
export const SETTINGS_VISIBLE_SET = 'SETTINGS_VISIBLE/SET';
export interface SETTINGS_VISIBLE_SET_ACTION {
type: typeof SETTINGS_VISIBLE_SET;
state: boolean;
}
export const USER_SUBSCRIPTION_SET = 'USER_SUBSCRIPTION/SET';
export interface USER_SUBSCRIPTION_SET_ACTION {
@@ -71,5 +64,4 @@ export type USER_ACTIONS =
| USER_HIDELIST_SET_ACTION
| USER_HIDE_ACTION
| USER_UNHIDE_ACTION
| SETTINGS_VISIBLE_SET_ACTION
| USER_SUBSCRIPTION_SET_ACTION;
+24
View File
@@ -0,0 +1,24 @@
import parseQuery from './parseQuery';
describe('parseQuery', () => {
it('should return empty object', () => {
expect(parseQuery('')).toEqual({});
expect(parseQuery('?')).toEqual({});
});
it('should add empty field to object', () => {
expect(parseQuery('?a')).toEqual({ a: '' });
});
it('should add empty field and field with param to object', () => {
expect(parseQuery('?a&b=1')).toEqual({ a: '', b: '1' });
});
it('should add all params to object', () => {
expect(parseQuery('?a=1&b=1')).toEqual({ a: '1', b: '1' });
});
it('should convert urlencoded param', () => {
expect(parseQuery('?x=%D1%8B%D1%84%D0%B2%D0%B0%D1%84%D1%8B%D0%B2%D1%84%D1%8B')).toEqual({ x: 'ыфвафывфы' });
});
});
+10 -14
View File
@@ -1,20 +1,16 @@
/** converts widnow.location.search into object */
export function parseQuery(search: string): { [key: string]: string } {
export default function parseQuery(search: string = window.location.search) {
if (search.length < 2) return {};
return search
.substr(1)
.split('&')
.map((chunk): [string, string] => {
const parts = chunk.split('=');
if (parts.length < 2) {
parts[1] = '';
} else {
parts[1] = decodeURIComponent(parts[1]);
}
return parts as [string, string];
})
.reduce<{ [key: string]: string }>((c, x) => {
c[x[0]] = x[1];
return c;
}, {});
.reduce((accum, param) => {
const [key, value] = param.split('=');
return {
...accum,
[key]: value ? decodeURIComponent(value) : '',
};
}, {} as Record<string, string>);
}
+2 -1
View File
@@ -7,7 +7,8 @@ export type Message =
user: User;
}
| { isUserInfoShown: false }
| { scrollTo: number };
| { scrollTo: number }
| { remarkIframeHeight: number };
/**
* Sends message to parent window
+10 -7
View File
@@ -1,6 +1,7 @@
<html>
<title>Privacy Policy</title>
<head>
<title>Privacy Policy</title>
</head>
<body>
<h1>Privacy Policy</h1>
<p>
@@ -39,10 +40,12 @@
<h3>Service Providers</h3>
<p>
We may employ third-party companies and individuals due to the following reasons:
<li>To facilitate our Service</li>
<li>To provide the Service on our behalf</li>
<li>To perform Service-related services</li>
<li>To assist us in analyzing how our Service is used</li>
<ul>
<li>To facilitate our Service</li>
<li>To provide the Service on our behalf</li>
<li>To perform Service-related services</li>
<li>To assist us in analyzing how our Service is used</li>
</ul>
</p>
<p>
@@ -85,4 +88,4 @@
<p>If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.</p>
</body>
</html>
</html>