Hide user comments (#334)
* add ability to hide user via localstorage * remove closures to avoid reconcilation * add actionBinder utilities * add user hide/show feature * remove unused type * fix css for settings user id * remove pointless confirmation for hide/show user in settings * add redux-dev-tools support
This commit is contained in:
@@ -27,6 +27,9 @@ export const PROVIDER_NAMES: { [P in AuthProvider['name']]: string } = {
|
||||
/** locastorage key for collapsed comments */
|
||||
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';
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ const DefaultProps: Partial<Props> = {
|
||||
url: 'https://example.com',
|
||||
count: 3,
|
||||
},
|
||||
hiddenUsers: {},
|
||||
};
|
||||
|
||||
describe('<AuthPanel />', () => {
|
||||
@@ -65,6 +66,39 @@ describe('<AuthPanel />', () => {
|
||||
expect(providerLinks[0].textContent).toEqual('Google');
|
||||
expect(providerLinks[1].textContent).toEqual('GitHub');
|
||||
});
|
||||
|
||||
it('should not render settings if there is no hidden users', () => {
|
||||
const element = (
|
||||
<AuthPanel
|
||||
{...DefaultProps as Props}
|
||||
user={null}
|
||||
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
|
||||
/>
|
||||
);
|
||||
|
||||
render(element, container);
|
||||
|
||||
const adminAction = container.querySelector('.auth-panel__admin-action')!;
|
||||
|
||||
expect(adminAction).toBe(null);
|
||||
});
|
||||
|
||||
it('should render settings if there is some hidden users', () => {
|
||||
const element = (
|
||||
<AuthPanel
|
||||
{...DefaultProps as Props}
|
||||
user={null}
|
||||
postInfo={{ ...DefaultProps.postInfo, read_only: true } as PostInfo}
|
||||
hiddenUsers={{ hidden_joe: {} as any }}
|
||||
/>
|
||||
);
|
||||
|
||||
render(element, container);
|
||||
|
||||
const adminAction = container.querySelector('.auth-panel__admin-action')!;
|
||||
|
||||
expect(adminAction.textContent).toEqual('Show settings');
|
||||
});
|
||||
});
|
||||
describe('For authorized user', () => {
|
||||
let container: HTMLElement;
|
||||
@@ -101,7 +135,7 @@ describe('<AuthPanel />', () => {
|
||||
|
||||
const adminAction = container.querySelector('.auth-panel__admin-action')!;
|
||||
|
||||
expect(adminAction.textContent).toEqual('Show blocked users');
|
||||
expect(adminAction.textContent).toEqual('Show settings');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,9 +11,11 @@ import Dropdown, { DropdownItem } from '@app/components/dropdown';
|
||||
import { Button } from '@app/components/button';
|
||||
import { UserID } from './__user-id';
|
||||
import { AnonymousLoginForm } from './__anonymous-login-form';
|
||||
import { StoreState } from '@app/store';
|
||||
|
||||
export interface Props {
|
||||
user: User | null;
|
||||
hiddenUsers: StoreState['hiddenUsers'];
|
||||
providers: (AuthProvider['name'])[];
|
||||
sort: Sorting;
|
||||
isCommentsDisabled: boolean;
|
||||
@@ -108,6 +110,8 @@ export class AuthPanel extends Component<Props, State> {
|
||||
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_';
|
||||
const isSettingsLabelVisible =
|
||||
Object.keys(this.props.hiddenUsers).length > 0 || (user && user.admin) || this.state.isBlockedVisible;
|
||||
|
||||
return (
|
||||
<div className={b('auth-panel', {}, { theme: props.theme, loggedIn })}>
|
||||
@@ -205,17 +209,17 @@ export class AuthPanel extends Component<Props, State> {
|
||||
)}
|
||||
|
||||
<div className="auth-panel__column">
|
||||
{user && user.admin && (
|
||||
{isSettingsLabelVisible && (
|
||||
<span
|
||||
className="auth-panel__pseudo-link auth-panel__admin-action"
|
||||
{...getHandleClickProps(() => this.toggleBlockedVisibility())}
|
||||
role="link"
|
||||
>
|
||||
{isBlockedVisible ? 'Hide' : 'Show'} blocked users
|
||||
{isBlockedVisible ? 'Hide' : 'Show'} settings
|
||||
</span>
|
||||
)}
|
||||
|
||||
{user && user.admin && ' • '}
|
||||
{isSettingsLabelVisible && ' • '}
|
||||
|
||||
{user && user.admin && (
|
||||
<span
|
||||
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
.blocked-users__list-item_view_invisible {
|
||||
.blocked-users__username {
|
||||
opacity: 0.5;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.blocked-users__list-item {
|
||||
cursor: default;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
.blocked-users__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.blocked-users__list {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.blocked-users__username {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.blocked-users_theme_dark {
|
||||
.blocked-users__action {
|
||||
&::before {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.blocked-users__user-id {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.blocked-users_theme_light {
|
||||
.blocked-users__action {
|
||||
&::before {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.blocked-users__user-id {
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, BlockedUser, Theme, BlockTTL } from '@app/common/types';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
|
||||
interface Props {
|
||||
theme: Theme;
|
||||
users: BlockedUser[];
|
||||
blockUser(id: User['id'], name: string, ttl: BlockTTL): Promise<void>;
|
||||
unblockUser(id: User['id']): Promise<void>;
|
||||
onUnblockSomeone(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
/**
|
||||
* cached copy so we can
|
||||
* reapply block on unblocked user
|
||||
*/
|
||||
users: BlockedUser[];
|
||||
unblockedUsers: (User['id'])[];
|
||||
}
|
||||
|
||||
export default class BlockedUsers extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
users: props.users.slice(),
|
||||
unblockedUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
block(user: BlockedUser) {
|
||||
if (confirm(`Do you want to block ${user.name}?`)) {
|
||||
this.setState({
|
||||
unblockedUsers: this.state.unblockedUsers.filter(x => x !== user.id),
|
||||
});
|
||||
this.props.blockUser(user.id, user.name, 'permanently');
|
||||
}
|
||||
}
|
||||
|
||||
unblock(user: BlockedUser) {
|
||||
if (confirm(`Do you want to unblock ${user.name}?`)) {
|
||||
this.setState({ unblockedUsers: this.state.unblockedUsers.concat([user.id]) });
|
||||
this.props.unblockUser(user.id);
|
||||
this.props.onUnblockSomeone();
|
||||
}
|
||||
}
|
||||
|
||||
render({ theme }: RenderableProps<Props>, { users, unblockedUsers }: State) {
|
||||
return (
|
||||
<div className={b('blocked-users', {}, { theme })} role="region" aria-label="Blocked users">
|
||||
{!users.length && <p>There are no blocked users.</p>}
|
||||
|
||||
{!!users.length && <p>List of blocked users:</p>}
|
||||
|
||||
{!!users.length && (
|
||||
<ul className="blocked-users__list">
|
||||
{users.map(user => {
|
||||
const isUserUnblocked = unblockedUsers.includes(user.id);
|
||||
|
||||
return (
|
||||
<li className={b('blocked-users__list-item', {}, { view: isUserUnblocked ? 'invisible' : null })}>
|
||||
<span className="blocked-users__username">{user.name}</span>{' '}
|
||||
<span className="blocked-users__user-id">({user.id})</span>
|
||||
<span className="blocked-users__user-block-ttl"> {formatTime(new Date(user.time))}</span>
|
||||
{isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.block(user))} className="blocked-users__action">
|
||||
block
|
||||
</span>
|
||||
)}
|
||||
{!isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.unblock(user))} className="blocked-users__action">
|
||||
unblock
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
function formatTime(time: Date): string {
|
||||
// let's assume that if block ttl is more than 50 years then user blocked permanently
|
||||
if (time.getFullYear() - currentYear >= 50) return 'permanently';
|
||||
|
||||
// 'ru-RU' adds a dot as a separator
|
||||
const date = time.toLocaleDateString(['ru-RU'], { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||
|
||||
// do it manually because Intl API doesn't add leading zeros to hours; idk why
|
||||
const hours = `0${time.getHours()}`.slice(-2);
|
||||
const mins = `0${time.getMinutes()}`.slice(-2);
|
||||
|
||||
return `until ${date} at ${hours}:${mins}`;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import withTheme from '../../components/with-theme';
|
||||
import BlockedUsers from './blocked-users';
|
||||
|
||||
export default withTheme(BlockedUsers);
|
||||
|
||||
require('./blocked-users.scss');
|
||||
|
||||
require('./__action/blocked-users__action.scss');
|
||||
require('./__list/blocked-users__list.scss');
|
||||
|
||||
require('./__list-item/blocked-users__list-item.scss');
|
||||
require('./__list-item/_view/_invisible/blocked-users__list-item_view_invisible.scss');
|
||||
|
||||
require('./__username/blocked-users__username.scss');
|
||||
|
||||
require('./_theme/_dark/blocked-users_theme_dark.scss');
|
||||
require('./_theme/_light/blocked-users_theme_light.scss');
|
||||
@@ -174,20 +174,26 @@ describe('<Comment />', () => {
|
||||
container = domContainer;
|
||||
});
|
||||
|
||||
it('visible for admin', () => {
|
||||
it('for admin if shows admin controls', () => {
|
||||
const element = <Comment {...{ ...DefaultProps, user: { ...DefaultProps.user, admin: true } } as Props} />;
|
||||
render(element, container);
|
||||
|
||||
const controls = container.querySelector('.comment__controls');
|
||||
expect(controls).not.toBe(null);
|
||||
const controls = container.querySelectorAll('.comment__controls > span');
|
||||
expect(controls!.length).toBe(5);
|
||||
expect(controls![0].textContent).toBe('Copy');
|
||||
expect(controls![1].textContent).toBe('Pin');
|
||||
expect(controls![2].textContent).toBe('Hide');
|
||||
expect(controls![3].childNodes[0].textContent).toBe('Block');
|
||||
expect(controls![4].textContent).toBe('Delete');
|
||||
});
|
||||
|
||||
it('not visible for regular user', () => {
|
||||
it('for regular user it shows only "hide"', () => {
|
||||
const element = <Comment {...{ ...DefaultProps, user: { ...DefaultProps.user, admin: false } } as Props} />;
|
||||
render(element, container);
|
||||
|
||||
const controls = container.querySelector('.comment__controls');
|
||||
expect(controls).toBe(null);
|
||||
const controls = container.querySelectorAll('.comment__controls > span');
|
||||
expect(controls!.length).toBe(1);
|
||||
expect(controls![0].textContent).toBe('Hide');
|
||||
});
|
||||
|
||||
it('verification badge clickable for admin', () => {
|
||||
|
||||
@@ -11,15 +11,16 @@ import { API_BASE, BASE_URL, COMMENT_NODE_CLASSNAME_PREFIX, BLOCKING_DURATIONS }
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import debounce from '@app/utils/debounce';
|
||||
import copy from '@app/common/copy';
|
||||
import { Theme, BlockTTL, Comment as CommentType, PostInfo, User, CommentMode, Image } from '@app/common/types';
|
||||
import { Theme, BlockTTL, Comment as CommentType, PostInfo, User, CommentMode } from '@app/common/types';
|
||||
import { extractErrorMessageFromResponse, FetcherError } from '@app/utils/errorUtils';
|
||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
||||
|
||||
import { Input } from '@app/components/input';
|
||||
import { AvatarIcon } from '@app/components/avatar-icon';
|
||||
import Countdown from '../countdown';
|
||||
import { boundActions } from './connected-comment';
|
||||
|
||||
export interface Props {
|
||||
export type Props = {
|
||||
user: User | null;
|
||||
data: CommentType;
|
||||
repliesCount?: number;
|
||||
@@ -42,21 +43,7 @@ export interface Props {
|
||||
theme: Theme;
|
||||
level?: number;
|
||||
mix?: string;
|
||||
|
||||
// actions are optional, as component has read-only mode, such as in last comments
|
||||
addComment?: (text: string, title: string, pid?: CommentType['id']) => Promise<void>;
|
||||
updateComment?: (id: CommentType['id'], text: string) => Promise<void>;
|
||||
removeComment?(id: CommentType['id']): Promise<void>;
|
||||
setReplyEditState?(id: CommentType['id'], mode: CommentMode): void;
|
||||
getPreview?: (text: string) => Promise<string>;
|
||||
putCommentVote?(id: CommentType['id'], value: number): Promise<void>;
|
||||
setCollapse?: (id: CommentType['id'], value: boolean) => void;
|
||||
setPinState?(id: CommentType['id'], value: boolean): Promise<void>;
|
||||
blockUser?(id: User['id'], name: User['name'], ttl: BlockTTL): Promise<void>;
|
||||
unblockUser?(id: User['id']): Promise<void>;
|
||||
setVerifyStatus?(id: User['id'], value: boolean): Promise<void>;
|
||||
uploadImage?(image: File): Promise<Image>;
|
||||
}
|
||||
} & Partial<typeof boundActions>;
|
||||
|
||||
export interface State {
|
||||
isCopied: boolean;
|
||||
@@ -105,7 +92,7 @@ export class Comment extends Component<Props, State> {
|
||||
this.updateState(nextProps);
|
||||
}
|
||||
|
||||
updateState(props: Props) {
|
||||
updateState = (props: Props) => {
|
||||
this.setState({
|
||||
scoreDelta: props.data.vote,
|
||||
cachedScore: props.data.score,
|
||||
@@ -125,52 +112,54 @@ export class Comment extends Component<Props, State> {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleReplying() {
|
||||
toggleReplying = () => {
|
||||
const { editMode } = this.props;
|
||||
if (editMode === CommentMode.Reply) {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
} else {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.Reply);
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.Reply });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleEditing() {
|
||||
toggleEditing = () => {
|
||||
const { editMode } = this.props;
|
||||
if (editMode === CommentMode.Edit) {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
} else {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.Edit);
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.Edit });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleUserInfoVisibility() {
|
||||
toggleUserInfoVisibility = () => {
|
||||
if (window.parent) {
|
||||
const { user } = this.props.data;
|
||||
const data = JSON.stringify({ isUserInfoShown: true, user });
|
||||
window.parent.postMessage(data, '*');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setPin(value: boolean) {
|
||||
togglePin = () => {
|
||||
const value = !this.props.data.pin;
|
||||
const promptMessage = `Do you want to ${value ? 'pin' : 'unpin'} this comment?`;
|
||||
|
||||
if (confirm(promptMessage)) {
|
||||
this.props.setPinState!(this.props.data.id, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setVerify(value: boolean) {
|
||||
toggleVerify = () => {
|
||||
const value = !this.props.data.user.verified;
|
||||
const userId = this.props.data.user.id;
|
||||
const promptMessage = `Do you want to ${value ? 'verify' : 'unverify'} this user?`;
|
||||
const promptMessage = `Do you want to ${value ? 'verify' : 'unverify'} ${this.props.data.user.name}?`;
|
||||
|
||||
if (confirm(promptMessage)) {
|
||||
this.props.setVerifyStatus!(userId, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onBlockUserClick(e: Event) {
|
||||
onBlockUserClick = (e: Event) => {
|
||||
// blur event will be triggered by the confirm pop-up which will start
|
||||
// infinite loop of blur -> confirm -> blur -> ...
|
||||
// so we trigger the blur event manually and have debounce mechanism to prevent it
|
||||
@@ -180,9 +169,9 @@ export class Comment extends Component<Props, State> {
|
||||
// we have to debounce the blockUser function calls otherwise it will be
|
||||
// called 2 times (by change event and by blur event)
|
||||
this.blockUser((e.target as HTMLOptionElement).value as BlockTTL);
|
||||
}
|
||||
};
|
||||
|
||||
blockUser(ttl: BlockTTL) {
|
||||
blockUser = (ttl: BlockTTL) => {
|
||||
const { user } = this.props.data;
|
||||
|
||||
const block_duration = BLOCKING_DURATIONS.find(el => el.value === ttl);
|
||||
@@ -194,9 +183,9 @@ export class Comment extends Component<Props, State> {
|
||||
if (confirm(`Do you want to block ${user.name} ${duration.toLowerCase()}?`)) {
|
||||
this.props.blockUser!(user.id, user.name, ttl);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onUnblockUserClick() {
|
||||
onUnblockUserClick = () => {
|
||||
const { user } = this.props.data;
|
||||
|
||||
const promptMessage = `Do you want to unblock this user?`;
|
||||
@@ -204,31 +193,36 @@ export class Comment extends Component<Props, State> {
|
||||
if (confirm(promptMessage)) {
|
||||
this.props.unblockUser!(user.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
deleteComment() {
|
||||
deleteComment = () => {
|
||||
if (confirm('Do you want to delete this comment?')) {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
|
||||
this.props.removeComment!(this.props.data.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleVoteError(e: FetcherError, originalScore: number, originalDelta: number) {
|
||||
hideUser = () => {
|
||||
if (!confirm(`Do you want to hide comments of ${this.props.data.user.name}?`)) return;
|
||||
this.props.hideUser!(this.props.data.user);
|
||||
};
|
||||
|
||||
handleVoteError = (e: FetcherError, originalScore: number, originalDelta: number) => {
|
||||
this.setState({
|
||||
scoreDelta: originalDelta,
|
||||
cachedScore: originalScore,
|
||||
voteErrorMessage: extractErrorMessageFromResponse(e),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
sendVotingRequest(votingValue: number, originalScore: number, originalDelta: number) {
|
||||
sendVotingRequest = (votingValue: number, originalScore: number, originalDelta: number) => {
|
||||
this.votingPromise = this.votingPromise
|
||||
.then(() => this.props.putCommentVote!(this.props.data.id, votingValue))
|
||||
.catch(e => this.handleVoteError(e, originalScore, originalDelta));
|
||||
}
|
||||
};
|
||||
|
||||
increaseScore() {
|
||||
increaseScore = () => {
|
||||
const { cachedScore, scoreDelta } = this.state;
|
||||
|
||||
if (scoreDelta === 1) return;
|
||||
@@ -240,9 +234,9 @@ export class Comment extends Component<Props, State> {
|
||||
});
|
||||
|
||||
this.sendVotingRequest(1, cachedScore, scoreDelta);
|
||||
}
|
||||
};
|
||||
|
||||
decreaseScore() {
|
||||
decreaseScore = () => {
|
||||
const { cachedScore, scoreDelta } = this.state;
|
||||
|
||||
if (scoreDelta === -1) return;
|
||||
@@ -254,21 +248,21 @@ export class Comment extends Component<Props, State> {
|
||||
});
|
||||
|
||||
this.sendVotingRequest(-1, cachedScore, scoreDelta);
|
||||
}
|
||||
};
|
||||
|
||||
async addComment(text: string, title: string, pid?: CommentType['id']) {
|
||||
addComment = async (text: string, title: string, pid?: CommentType['id']) => {
|
||||
await this.props.addComment!(text, title, pid);
|
||||
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
}
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
};
|
||||
|
||||
async updateComment(id: CommentType['id'], text: string) {
|
||||
updateComment = async (id: CommentType['id'], text: string) => {
|
||||
await this.props.updateComment!(id, text);
|
||||
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
}
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
};
|
||||
|
||||
scrollToParent(e: Event) {
|
||||
scrollToParent = (e: Event) => {
|
||||
const {
|
||||
data: { pid },
|
||||
} = this.props;
|
||||
@@ -280,15 +274,17 @@ export class Comment extends Component<Props, State> {
|
||||
if (parentCommentNode) {
|
||||
parentCommentNode.scrollIntoView();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
toggleCollapse() {
|
||||
this.props.setReplyEditState!(this.props.data.id, CommentMode.None);
|
||||
toggleCollapse = () => {
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
|
||||
this.props.setCollapse!(this.props.data.id, !this.props.collapsed);
|
||||
}
|
||||
};
|
||||
|
||||
copyComment({ username, time }: { username: string; time: string }) {
|
||||
copyComment = () => {
|
||||
const username = this.props.data.user.name;
|
||||
const time = this.props.data.time;
|
||||
const text = this.textNode!.textContent || '';
|
||||
|
||||
copy(`<b>${username}</b> ${time}<br>${text.replace(/\n+/g, '<br>')}`);
|
||||
@@ -296,42 +292,42 @@ export class Comment extends Component<Props, State> {
|
||||
this.setState({ isCopied: true }, () => {
|
||||
setTimeout(() => this.setState({ isCopied: false }), 3000);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines whether current client is admin
|
||||
*/
|
||||
isAdmin(): boolean {
|
||||
isAdmin = (): boolean => {
|
||||
return !!this.props.user && this.props.user.admin;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines whether current client is not logged in
|
||||
*/
|
||||
isGuest(): boolean {
|
||||
isGuest = (): boolean => {
|
||||
return !this.props.user;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines whether current client is logged in via `Anonymous provider`
|
||||
*/
|
||||
isAnonymous(): boolean {
|
||||
isAnonymous = (): boolean => {
|
||||
return isUserAnonymous(this.props.user);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Defines whether comment made by logged in user
|
||||
*/
|
||||
isCurrentUser(): boolean {
|
||||
isCurrentUser = (): boolean => {
|
||||
if (this.isGuest()) return false;
|
||||
|
||||
return this.props.data.user.id === this.props.user!.id;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns reason for disabled downvoting
|
||||
*/
|
||||
getDownvoteDisabledReason(): string | null {
|
||||
getDownvoteDisabledReason = (): string | null => {
|
||||
if (!(this.props.view === 'main' || this.props.view === 'pinned')) return "Voting allowed only on post's page";
|
||||
if (this.props.post_info!.read_only) return "Can't vote on read-only topics";
|
||||
if (this.props.data.delete) return "Can't vote for deleted comment";
|
||||
@@ -340,12 +336,12 @@ export class Comment extends Component<Props, State> {
|
||||
if (this.isGuest()) return 'Sign in to vote';
|
||||
if (this.isAnonymous()) return "Anonymous users can't vote";
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* returns reason for disabled upvoting
|
||||
*/
|
||||
getUpvoteDisabledReason(): string | null {
|
||||
getUpvoteDisabledReason = (): string | null => {
|
||||
if (!(this.props.view === 'main' || this.props.view === 'pinned')) return "Voting allowed only on post's page";
|
||||
if (this.props.post_info!.read_only) return "Can't vote on read-only topics";
|
||||
if (this.props.data.delete) return "Can't vote for deleted comment";
|
||||
@@ -353,7 +349,83 @@ export class Comment extends Component<Props, State> {
|
||||
if (this.isGuest()) return 'Sign in to vote';
|
||||
if (this.isAnonymous()) return "Anonymous users can't vote";
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
getCommentControls = (): JSX.Element[] => {
|
||||
const isAdmin = this.isAdmin();
|
||||
const isCurrentUser = this.isCurrentUser();
|
||||
const controls: JSX.Element[] = [];
|
||||
|
||||
if (this.props.data.delete) {
|
||||
return controls;
|
||||
}
|
||||
|
||||
if (!(this.props.view === 'main' || this.props.view === 'pinned')) {
|
||||
return controls;
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
controls.push(
|
||||
this.state.isCopied ? (
|
||||
<span className="comment__control comment__control_view_inactive">Copied!</span>
|
||||
) : (
|
||||
<span {...getHandleClickProps(this.copyComment)} className="comment__control">
|
||||
Copy
|
||||
</span>
|
||||
)
|
||||
);
|
||||
|
||||
controls.push(
|
||||
<span {...getHandleClickProps(this.togglePin)} className="comment__control">
|
||||
{this.props.data.pin ? 'Unpin' : 'Pin'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isCurrentUser) {
|
||||
controls.push(
|
||||
<span {...getHandleClickProps(this.hideUser)} className="comment__control">
|
||||
Hide
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
if (this.props.isUserBanned) {
|
||||
controls.push(
|
||||
<span {...getHandleClickProps(this.onUnblockUserClick)} className="comment__control">
|
||||
Unblock
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.user!.id !== this.props.data.user.id && !this.props.isUserBanned) {
|
||||
controls.push(
|
||||
<span className="comment__control comment__control_select-label">
|
||||
Block
|
||||
<select className="comment__control_select" onBlur={this.onBlockUserClick} onChange={this.onBlockUserClick}>
|
||||
<option disabled selected value={undefined}>
|
||||
{' '}
|
||||
Blocking period{' '}
|
||||
</option>
|
||||
{BLOCKING_DURATIONS.map(block => (
|
||||
<option value={block.value}>{block.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.props.data.delete) {
|
||||
controls.push(
|
||||
<span {...getHandleClickProps(this.deleteComment)} className="comment__control">
|
||||
Delete
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
return controls;
|
||||
};
|
||||
|
||||
render(props: RenderableProps<Props>, state: State) {
|
||||
const isAdmin = this.isAdmin();
|
||||
@@ -371,6 +443,7 @@ export class Comment extends Component<Props, State> {
|
||||
const editable = props.repliesCount === 0 && state.editDeadline;
|
||||
const scoreSignEnabled = !StaticStore.config.positive_score;
|
||||
const uploadImageHandler = this.isAnonymous() ? undefined : this.props.uploadImage;
|
||||
const commentControls = this.getCommentControls();
|
||||
|
||||
/**
|
||||
* CommentType adapted for rendering
|
||||
@@ -474,7 +547,7 @@ export class Comment extends Component<Props, State> {
|
||||
|
||||
{props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(() => this.toggleUserInfoVisibility())}
|
||||
{...getHandleClickProps(this.toggleUserInfoVisibility)}
|
||||
className="comment__username"
|
||||
title={o.user.id}
|
||||
>
|
||||
@@ -484,7 +557,7 @@ export class Comment extends Component<Props, State> {
|
||||
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(() => this.setVerify(!o.user.verified))}
|
||||
{...getHandleClickProps(this.toggleVerify)}
|
||||
aria-label="Toggle verification"
|
||||
title={o.user.verified ? 'Verified user' : 'Unverified user'}
|
||||
className={b('comment__verification', {}, { active: o.user.verified, clickable: true })}
|
||||
@@ -517,7 +590,7 @@ export class Comment extends Component<Props, State> {
|
||||
|
||||
{!props.disabled && props.view === 'main' && (
|
||||
<span
|
||||
{...getHandleClickProps(() => this.toggleCollapse())}
|
||||
{...getHandleClickProps(this.toggleCollapse)}
|
||||
className={b('comment__action', {}, { type: 'collapse', selected: props.collapsed })}
|
||||
>
|
||||
{props.collapsed ? '+' : '−'}
|
||||
@@ -532,7 +605,7 @@ export class Comment extends Component<Props, State> {
|
||||
{ type: 'up', selected: state.scoreDelta === 1, disabled: isUpvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === 1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isUpvotingDisabled ? undefined : () => this.increaseScore())}
|
||||
{...getHandleClickProps(isUpvotingDisabled ? undefined : this.increaseScore)}
|
||||
title={upvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote up
|
||||
@@ -550,7 +623,7 @@ export class Comment extends Component<Props, State> {
|
||||
{ type: 'down', selected: state.scoreDelta === -1, disabled: isDownvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === -1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isDownvotingDisabled ? undefined : () => this.decreaseScore())}
|
||||
{...getHandleClickProps(isDownvotingDisabled ? undefined : this.decreaseScore)}
|
||||
title={downvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote down
|
||||
@@ -575,7 +648,7 @@ export class Comment extends Component<Props, State> {
|
||||
{(!props.collapsed || props.view === 'pinned') && (
|
||||
<div className="comment__actions">
|
||||
{!props.data.delete && !props.isCommentsDisabled && !props.disabled && !isGuest && props.view === 'main' && (
|
||||
<span {...getHandleClickProps(() => this.toggleReplying())} className="comment__action">
|
||||
<span {...getHandleClickProps(this.toggleReplying)} className="comment__action">
|
||||
{isReplying ? 'Cancel' : 'Reply'}
|
||||
</span>
|
||||
)}
|
||||
@@ -587,14 +660,14 @@ export class Comment extends Component<Props, State> {
|
||||
(editable || isEditing) &&
|
||||
props.view === 'main' && [
|
||||
<span
|
||||
{...getHandleClickProps(() => this.toggleEditing())}
|
||||
{...getHandleClickProps(this.toggleEditing)}
|
||||
className="comment__action comment__action_type_edit"
|
||||
>
|
||||
{isEditing ? 'Cancel' : 'Edit'}
|
||||
</span>,
|
||||
!isAdmin && (
|
||||
<span
|
||||
{...getHandleClickProps(() => this.deleteComment())}
|
||||
{...getHandleClickProps(this.deleteComment)}
|
||||
className="comment__action comment__action_type_delete"
|
||||
>
|
||||
Delete
|
||||
@@ -613,57 +686,7 @@ export class Comment extends Component<Props, State> {
|
||||
),
|
||||
]}
|
||||
|
||||
{!props.data.delete && isAdmin && (
|
||||
<span className="comment__controls">
|
||||
{!state.isCopied && (
|
||||
<span
|
||||
{...getHandleClickProps(() => this.copyComment({ username: o.user.name, time: o.time }))}
|
||||
className="comment__control"
|
||||
>
|
||||
Copy
|
||||
</span>
|
||||
)}
|
||||
|
||||
{state.isCopied && <span className="comment__control comment__control_view_inactive">Copied!</span>}
|
||||
|
||||
{(props.view === 'main' || props.view === 'pinned') && (
|
||||
<span {...getHandleClickProps(() => this.setPin(!props.data.pin))} className="comment__control">
|
||||
{props.data.pin ? 'Unpin' : 'Pin'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{props.isUserBanned && (
|
||||
<span {...getHandleClickProps(() => this.onUnblockUserClick())} className="comment__control">
|
||||
Unblock
|
||||
</span>
|
||||
)}
|
||||
|
||||
{props.user!.id !== props.data.user.id && !props.isUserBanned && (
|
||||
<span className="comment__control comment__control_select-label">
|
||||
Block
|
||||
<select
|
||||
className="comment__control_select"
|
||||
onBlur={e => this.onBlockUserClick(e)}
|
||||
onChange={e => this.onBlockUserClick(e)}
|
||||
>
|
||||
<option disabled selected value={undefined}>
|
||||
{' '}
|
||||
Blocking period{' '}
|
||||
</option>
|
||||
{BLOCKING_DURATIONS.map(block => (
|
||||
<option value={block.value}>{block.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!props.data.delete && (
|
||||
<span {...getHandleClickProps(() => this.deleteComment())} className="comment__control">
|
||||
Delete
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{commentControls.length > 0 && <span className="comment__controls">{commentControls}</span>}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
* and should be importded explicitly
|
||||
*/
|
||||
|
||||
import { Comment as CommentType, User, BlockTTL, CommentMode } from '@app/common/types';
|
||||
import { Comment as CommentType } from '@app/common/types';
|
||||
|
||||
import { connect } from 'preact-redux';
|
||||
|
||||
import { StoreState, StoreDispatch } from '@app/store';
|
||||
import { StoreState } from '@app/store';
|
||||
import {
|
||||
addComment,
|
||||
removeComment,
|
||||
@@ -17,14 +17,15 @@ import {
|
||||
setCommentMode,
|
||||
} from '@app/store/comments/actions';
|
||||
import { setCollapse } from '@app/store/thread/actions';
|
||||
import { blockUser, unblockUser, setVirifiedStatus } from '@app/store/user/actions';
|
||||
import { blockUser, unblockUser, hideUser, setVerifiedStatus } from '@app/store/user/actions';
|
||||
|
||||
import { Comment, Props } from './comment';
|
||||
import { getCommentMode } from '@app/store/comments/getters';
|
||||
import { uploadImage } from '@app/common/api';
|
||||
import { uploadImage, getPreview } from '@app/common/api';
|
||||
import { getThreadIsCollapsed } from '@app/store/thread/getters';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
|
||||
const mapProps = (state: StoreState, cprops: { data: CommentType }) => {
|
||||
const mapStateToProps = (state: StoreState, cprops: { data: CommentType }) => {
|
||||
const props: Pick<
|
||||
Props,
|
||||
'editMode' | 'user' | 'isUserBanned' | 'post_info' | 'isCommentsDisabled' | 'theme' | 'collapsed'
|
||||
@@ -40,41 +41,24 @@ const mapProps = (state: StoreState, cprops: { data: CommentType }) => {
|
||||
return props;
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch: StoreDispatch) => {
|
||||
const props: Pick<
|
||||
Props,
|
||||
| 'addComment'
|
||||
| 'updateComment'
|
||||
| 'removeComment'
|
||||
| 'setReplyEditState'
|
||||
| 'setCollapse'
|
||||
| 'setPinState'
|
||||
| 'putCommentVote'
|
||||
| 'blockUser'
|
||||
| 'unblockUser'
|
||||
| 'setVerifyStatus'
|
||||
| 'uploadImage'
|
||||
> = {
|
||||
addComment: (text: string, title: string, pid?: CommentType['id']) => dispatch(addComment(text, title, pid)),
|
||||
updateComment: (id: CommentType['id'], text: string) => dispatch(updateComment(id, text)),
|
||||
removeComment: (id: CommentType['id']) => dispatch(removeComment(id)),
|
||||
setReplyEditState: (id: CommentType['id'], mode: CommentMode) => dispatch(setCommentMode({ id, state: mode })),
|
||||
setCollapse: (id: CommentType['id'], value: boolean) => dispatch(setCollapse(id, value)),
|
||||
setPinState: (id: CommentType['id'], value: boolean) => dispatch(setPinState(id, value)),
|
||||
putCommentVote: (id: CommentType['id'], value: number) => dispatch(putVote(id, value)),
|
||||
|
||||
blockUser: (id: User['id'], name: User['name'], ttl: BlockTTL) => dispatch(blockUser(id, name, ttl)),
|
||||
unblockUser: (id: User['id']) => dispatch(unblockUser(id)),
|
||||
setVerifyStatus: (id: User['id'], value: boolean) => dispatch(setVirifiedStatus(id, value)),
|
||||
// should i made it as store action?
|
||||
uploadImage: (image: File) => uploadImage(image),
|
||||
};
|
||||
|
||||
return props;
|
||||
};
|
||||
export const boundActions = bindActions({
|
||||
addComment,
|
||||
updateComment,
|
||||
removeComment,
|
||||
setReplyEditState: setCommentMode,
|
||||
setCollapse,
|
||||
setPinState,
|
||||
putCommentVote: putVote,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
hideUser,
|
||||
setVerifyStatus: setVerifiedStatus,
|
||||
uploadImage,
|
||||
getPreview,
|
||||
});
|
||||
|
||||
/** Comment component connected to redux */
|
||||
export const ConnectedComment = connect(
|
||||
mapProps,
|
||||
mapDispatchToProps
|
||||
mapStateToProps,
|
||||
boundActions as Partial<typeof boundActions>
|
||||
)(Comment);
|
||||
|
||||
@@ -9,12 +9,9 @@ import {
|
||||
PostInfo,
|
||||
BlockedUser,
|
||||
Comment as CommentType,
|
||||
Tree,
|
||||
Sorting,
|
||||
Theme,
|
||||
AuthProvider,
|
||||
BlockTTL,
|
||||
Image,
|
||||
} from '@app/common/types';
|
||||
import {
|
||||
NODE_ID,
|
||||
@@ -26,7 +23,7 @@ import {
|
||||
import { maxShownComments } from '@app/common/settings';
|
||||
|
||||
import { StaticStore } from '@app/common/static_store';
|
||||
import { StoreState, StoreDispatch } from '@app/store';
|
||||
import { StoreState } from '@app/store';
|
||||
import {
|
||||
fetchUser,
|
||||
logout,
|
||||
@@ -34,7 +31,9 @@ import {
|
||||
blockUser,
|
||||
unblockUser,
|
||||
fetchBlockedUsers,
|
||||
setBlockedVisibleState,
|
||||
setSettingsVisibleState,
|
||||
hideUser,
|
||||
unhideUser,
|
||||
} from '@app/store/user/actions';
|
||||
import { fetchComments } from '@app/store/comments/actions';
|
||||
import { setCommentsReadOnlyState } from '@app/store/post_info/actions';
|
||||
@@ -43,41 +42,47 @@ import { setSort } from '@app/store/sort/actions';
|
||||
import { addComment, updateComment } from '@app/store/comments/actions';
|
||||
|
||||
import { AuthPanel } from '@app/components/auth-panel';
|
||||
import BlockedUsers from '@app/components/blocked-users';
|
||||
import Settings from '@app/components/settings';
|
||||
import { ConnectedComment as Comment } from '@app/components/comment/connected-comment';
|
||||
import { Input } from '@app/components/input';
|
||||
import Preloader from '@app/components/preloader';
|
||||
import { Thread } from '@app/components/thread';
|
||||
import { uploadImage } from '@app/common/api';
|
||||
import { uploadImage, getPreview } from '@app/common/api';
|
||||
import { isUserAnonymous } from '@app/utils/isUserAnonymous';
|
||||
import { bindActions } from '@app/utils/actionBinder';
|
||||
|
||||
interface Props {
|
||||
const boundActions = bindActions({
|
||||
fetchComments,
|
||||
fetchUser,
|
||||
fetchBlockedUsers,
|
||||
setSettingsVisible: setSettingsVisibleState,
|
||||
logIn,
|
||||
logOut: logout,
|
||||
setTheme,
|
||||
getPreview,
|
||||
enableComments: () => setCommentsReadOnlyState(false),
|
||||
disableComments: () => setCommentsReadOnlyState(true),
|
||||
changeSort: setSort,
|
||||
blockUser,
|
||||
unblockUser,
|
||||
hideUser,
|
||||
unhideUser,
|
||||
addComment,
|
||||
updateComment,
|
||||
uploadImage,
|
||||
});
|
||||
|
||||
type Props = {
|
||||
user: User | null;
|
||||
sort: Sorting;
|
||||
comments: Node[];
|
||||
pinnedComments: CommentType[];
|
||||
theme: Theme;
|
||||
info: PostInfo;
|
||||
bannedUsers: BlockedUser[];
|
||||
isBlockedVisible: boolean;
|
||||
|
||||
fetchComments(sort: Sorting): Promise<Tree>;
|
||||
fetchUser(): Promise<User | null>;
|
||||
fetchBlockedUsers(): Promise<BlockedUser[]>;
|
||||
logIn(p: AuthProvider): Promise<User | null>;
|
||||
logOut(): Promise<void>;
|
||||
setTheme: (theme: Theme) => void;
|
||||
setBlockedVisible: (value: boolean) => boolean;
|
||||
changeSort(sort: Sorting): Promise<void>;
|
||||
enableComments(): Promise<boolean>;
|
||||
disableComments(): Promise<boolean>;
|
||||
getPreview(text: string): Promise<string>;
|
||||
blockUser(id: User['id'], name: User['name'], ttl: BlockTTL): Promise<void>;
|
||||
unblockUser(id: User['id']): Promise<void>;
|
||||
addComment(text: string, title: string, pid?: CommentType['id']): Promise<void>;
|
||||
updateComment(id: string, text: string): Promise<void>;
|
||||
uploadImage(image: File): Promise<Image>;
|
||||
}
|
||||
hiddenUsers: StoreState['hiddenUsers'];
|
||||
blockedUsers: BlockedUser[];
|
||||
isSettingsVisible: boolean;
|
||||
} & typeof boundActions;
|
||||
|
||||
interface State {
|
||||
isLoaded: boolean;
|
||||
@@ -159,21 +164,22 @@ export class Root extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
onBlockedUsersShow() {
|
||||
this.props.fetchBlockedUsers().then(() => {
|
||||
this.props.setBlockedVisible(true);
|
||||
});
|
||||
async onBlockedUsersShow() {
|
||||
if (this.props.user && this.props.user.admin) {
|
||||
await this.props.fetchBlockedUsers();
|
||||
}
|
||||
this.props.setSettingsVisible(true);
|
||||
}
|
||||
|
||||
onBlockedUsersHide() {
|
||||
async onBlockedUsersHide() {
|
||||
// if someone was unblocked let's reload comments
|
||||
if (this.state.wasSomeoneUnblocked) {
|
||||
this.props.fetchComments(this.props.sort);
|
||||
}
|
||||
this.props.setBlockedVisible(false),
|
||||
this.setState({
|
||||
wasSomeoneUnblocked: false,
|
||||
});
|
||||
this.props.setSettingsVisible(false);
|
||||
this.setState({
|
||||
wasSomeoneUnblocked: false,
|
||||
});
|
||||
}
|
||||
|
||||
async changeSort(sort: Sorting) {
|
||||
@@ -221,6 +227,7 @@ export class Root extends Component<Props, State> {
|
||||
<AuthPanel
|
||||
theme={this.props.theme}
|
||||
user={this.props.user}
|
||||
hiddenUsers={this.props.hiddenUsers}
|
||||
sort={this.props.sort}
|
||||
providers={StaticStore.config.auth_providers}
|
||||
isCommentsDisabled={isCommentsDisabled}
|
||||
@@ -234,7 +241,7 @@ export class Root extends Component<Props, State> {
|
||||
onSortChange={this.props.changeSort}
|
||||
/>
|
||||
|
||||
{!this.props.isBlockedVisible && (
|
||||
{!this.props.isSettingsVisible && (
|
||||
<div className="root__main">
|
||||
{!isGuest && !isCommentsDisabled && (
|
||||
<Input
|
||||
@@ -284,12 +291,16 @@ export class Root extends Component<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{this.props.isBlockedVisible && (
|
||||
{this.props.isSettingsVisible && (
|
||||
<div className="root__main">
|
||||
<BlockedUsers
|
||||
users={this.props.bannedUsers}
|
||||
<Settings
|
||||
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>
|
||||
@@ -307,38 +318,18 @@ export class Root extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch: StoreDispatch) => {
|
||||
return {
|
||||
fetchComments: (sort: Sorting) => dispatch(fetchComments(sort)),
|
||||
fetchUser: () => dispatch(fetchUser()),
|
||||
fetchBlockedUsers: () => dispatch(fetchBlockedUsers()),
|
||||
setBlockedVisible: (value: boolean) => dispatch(setBlockedVisibleState(value)),
|
||||
logIn: (provider: AuthProvider) => dispatch(logIn(provider)),
|
||||
logOut: () => dispatch(logout()),
|
||||
setTheme: (theme: Theme) => dispatch(setTheme(theme)),
|
||||
enableComments: () => dispatch(setCommentsReadOnlyState(false)),
|
||||
disableComments: () => dispatch(setCommentsReadOnlyState(true)),
|
||||
changeSort: (sort: Sorting) => dispatch(setSort(sort)),
|
||||
blockUser: (id: User['id'], name: User['name'], ttl: BlockTTL) => dispatch(blockUser(id, name, ttl)),
|
||||
unblockUser: (id: User['id']) => dispatch(unblockUser(id)),
|
||||
addComment: (text: string, pageTitle: string, pid?: CommentType['id']) =>
|
||||
dispatch(addComment(text, pageTitle, pid)),
|
||||
updateComment: (id: CommentType['id'], text: string) => dispatch(updateComment(id, text)),
|
||||
uploadImage: (image: File) => uploadImage(image),
|
||||
};
|
||||
};
|
||||
|
||||
/** Root component connected to redux */
|
||||
export const ConnectedRoot = connect(
|
||||
(state: StoreState) => ({
|
||||
user: state.user,
|
||||
sort: state.sort,
|
||||
isBlockedVisible: state.isBlockedVisible,
|
||||
isSettingsVisible: state.isSettingsVisible,
|
||||
comments: state.comments,
|
||||
pinnedComments: state.pinnedComments,
|
||||
theme: state.theme,
|
||||
info: state.info,
|
||||
bannedUsers: state.bannedUsers,
|
||||
hiddenUsers: state.hiddenUsers,
|
||||
blockedUsers: state.bannedUsers,
|
||||
}),
|
||||
mapDispatchToProps
|
||||
boundActions
|
||||
)(Root);
|
||||
|
||||
+1
-5
@@ -1,13 +1,9 @@
|
||||
.blocked-users__action {
|
||||
.settings__action {
|
||||
margin-left: 8px;
|
||||
font-weight: 700;
|
||||
color: #0aa;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #06c5c5;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.settings__dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.settings__invisible {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.settings__list {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.settings__list-item {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
@media (hover: hover) {
|
||||
.settings__action {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.settings__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.settings__section + .settings__section {
|
||||
margin-top: 2em;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.settings__user-id {
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.settings__username {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.settings_theme_dark {
|
||||
.settings__action {
|
||||
&::before {
|
||||
color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.settings__blocked-users-username {
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.settings_theme_light {
|
||||
.settings__action {
|
||||
&::before {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.settings__blocked-users-username {
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import withTheme from '../../components/with-theme';
|
||||
import Settings from './settings';
|
||||
|
||||
export default withTheme(Settings);
|
||||
|
||||
require('./settings.scss');
|
||||
|
||||
require('./__action/settings__action.scss');
|
||||
require('./__section/settings__section.scss');
|
||||
require('./__list/settings__list.scss');
|
||||
require('./__invisible/settings__invisible.scss');
|
||||
require('./__dimmed/settings__dimmed.scss');
|
||||
require('./__username/settings__username.scss');
|
||||
require('./__user-id/settings__user-id.scss');
|
||||
require('./_theme/_dark/settings_theme_dark.scss');
|
||||
require('./_theme/_light/settings_theme_light.scss');
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
.blocked-users {
|
||||
.settings {
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/** @jsx h */
|
||||
import { h, Component, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { User, BlockedUser, Theme, BlockTTL } from '@app/common/types';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { StoreState } from '@app/store';
|
||||
|
||||
interface Props {
|
||||
theme: Theme;
|
||||
user: StoreState['user'];
|
||||
blockedUsers: BlockedUser[];
|
||||
hiddenUsers: StoreState['hiddenUsers'];
|
||||
blockUser(id: User['id'], name: string, ttl: BlockTTL): Promise<void>;
|
||||
unblockUser(id: User['id']): Promise<void>;
|
||||
hideUser(user: User): void;
|
||||
unhideUser(userid: User['id']): void;
|
||||
onUnblockSomeone(): void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
/**
|
||||
* cached copy so we can
|
||||
* reapply block on unblocked user
|
||||
*/
|
||||
blockedUsers: BlockedUser[];
|
||||
unblockedUsers: (User['id'])[];
|
||||
hiddenUsers: { [id: string]: User };
|
||||
unhiddenUsers: (User['id'])[];
|
||||
}
|
||||
|
||||
export default class BlockedUsers extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
blockedUsers: props.blockedUsers.slice(),
|
||||
unblockedUsers: [],
|
||||
hiddenUsers: { ...props.hiddenUsers },
|
||||
unhiddenUsers: [],
|
||||
};
|
||||
}
|
||||
|
||||
block = (user: BlockedUser) => {
|
||||
if (!confirm(`Do you want to block ${user.name}?`)) return;
|
||||
this.setState({
|
||||
unblockedUsers: this.state.unblockedUsers.filter(x => x !== user.id),
|
||||
});
|
||||
this.props.blockUser(user.id, user.name, 'permanently');
|
||||
};
|
||||
|
||||
unblock = (user: BlockedUser) => {
|
||||
if (!confirm(`Do you want to unblock ${user.name}?`)) return;
|
||||
this.setState({ unblockedUsers: this.state.unblockedUsers.concat([user.id]) });
|
||||
this.props.unblockUser(user.id);
|
||||
this.props.onUnblockSomeone();
|
||||
};
|
||||
|
||||
hide = (user: User) => {
|
||||
this.setState({
|
||||
unhiddenUsers: this.state.unhiddenUsers.filter(x => x !== user.id),
|
||||
});
|
||||
this.props.hideUser(user);
|
||||
};
|
||||
|
||||
unhide = (user: User) => {
|
||||
this.setState({ unhiddenUsers: this.state.unhiddenUsers.concat([user.id]) });
|
||||
this.props.unhideUser(user.id);
|
||||
this.props.onUnblockSomeone();
|
||||
};
|
||||
|
||||
__isUserHidden = (user: User): boolean => {
|
||||
if (this.state.unhiddenUsers.indexOf(user.id) === -1) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
render({ user, theme }: RenderableProps<Props>, { blockedUsers, unblockedUsers, unhiddenUsers }: State) {
|
||||
const hiddenUsersList = Object.values(this.state.hiddenUsers);
|
||||
return (
|
||||
<div className={b('settings', {}, { theme })}>
|
||||
<div className="settings__section settings__hidden-users" role="region" aria-label="Hidden users">
|
||||
<h3>Hidden users:</h3>
|
||||
{!hiddenUsersList.length && <h4 className="settings__dimmed">There are no hidden users.</h4>}
|
||||
{!!hiddenUsersList.length && (
|
||||
<ul className="settings__list">
|
||||
{hiddenUsersList.map(user => {
|
||||
const isUserUnhidden = unhiddenUsers.includes(user.id);
|
||||
|
||||
return (
|
||||
<li className="settings__list-item">
|
||||
<span
|
||||
className={['settings__username', isUserUnhidden ? 'settings__invisible' : null].join(' ')}
|
||||
title={user.id}
|
||||
>
|
||||
{user.name || 'unknown'}
|
||||
</span>
|
||||
{this.__isUserHidden(user) ? (
|
||||
<span className="settings__action" {...getHandleClickProps(() => this.unhide(user))}>
|
||||
show
|
||||
</span>
|
||||
) : (
|
||||
<span className="settings__action" {...getHandleClickProps(() => this.hide(user))}>
|
||||
hide
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="settings__user-id">
|
||||
id: <span>{user.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
{user && user.admin && (
|
||||
<div className="settings__section settings__blocked-users" role="region" aria-label="Blocked users">
|
||||
<h3>Blocked users:</h3>
|
||||
|
||||
{!blockedUsers.length && <h4 className="settings__dimmed">There are no blocked users.</h4>}
|
||||
|
||||
{!!blockedUsers.length && (
|
||||
<ul className="settings__list settings__blocked-users-list">
|
||||
{blockedUsers.map(user => {
|
||||
const isUserUnblocked = unblockedUsers.includes(user.id);
|
||||
|
||||
return (
|
||||
<li className="settings__list-item">
|
||||
<span
|
||||
className={['settings__username', isUserUnblocked ? 'settings__invisible' : null].join(' ')}
|
||||
title={user.id}
|
||||
>
|
||||
{user.name || 'unknown'}
|
||||
</span>
|
||||
<span className="settings__blocked-users-user-block-ttl"> {formatTime(new Date(user.time))}</span>
|
||||
{isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.block(user))} className="blocked-users__action">
|
||||
block
|
||||
</span>
|
||||
)}
|
||||
{!isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.unblock(user))} className="settings__action">
|
||||
unblock
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="settings__user-id">
|
||||
id: <span>{user.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
function formatTime(time: Date): string {
|
||||
// let's assume that if block ttl is more than 50 years then user blocked permanently
|
||||
if (time.getFullYear() - currentYear >= 50) return 'permanently';
|
||||
|
||||
// 'ru-RU' adds a dot as a separator
|
||||
const date = time.toLocaleDateString(['ru-RU'], { day: '2-digit', month: '2-digit', year: '2-digit' });
|
||||
|
||||
// do it manually because Intl API doesn't add leading zeros to hours; idk why
|
||||
const hours = `0${time.getHours()}`.slice(-2);
|
||||
const mins = `0${time.getMinutes()}`.slice(-2);
|
||||
|
||||
return `until ${date} at ${hours}:${mins}`;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ function Thread(props: RenderableProps<Props>) {
|
||||
role={['listitem'].concat(!collapsed && replies.length ? 'list' : []).join(' ')}
|
||||
aria-expanded={!collapsed}
|
||||
>
|
||||
<Comment view="main" data={comment} repliesCount={replies.length} level={level} getPreview={props.getPreview} />
|
||||
<Comment view="main" data={comment} repliesCount={replies.length} level={level} />
|
||||
|
||||
{!collapsed &&
|
||||
!!replies.length &&
|
||||
|
||||
@@ -16,6 +16,8 @@ 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 { bindActionCreators } from 'redux';
|
||||
import { fetchHiddenUsers } from './store/user/actions';
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
@@ -35,6 +37,9 @@ async function init(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
const boundFetchHiddenUsers = bindActionCreators(fetchHiddenUsers, reduxStore.dispatch);
|
||||
boundFetchHiddenUsers();
|
||||
|
||||
const params = window.location.search
|
||||
.replace(/^\?/, '')
|
||||
.split('&')
|
||||
@@ -63,7 +68,7 @@ async function init(): Promise<void> {
|
||||
} else {
|
||||
render(
|
||||
<Provider store={reduxStore}>
|
||||
<ConnectedRoot getPreview={api.getPreview} />
|
||||
<ConnectedRoot />
|
||||
</Provider>,
|
||||
node.parentElement!,
|
||||
node
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
replaceComment as uReplaceComment,
|
||||
removeComment as uRemoveComment,
|
||||
setCommentPin as uSetCommentPin,
|
||||
filterTree,
|
||||
} from './utils';
|
||||
import { COMMENTS_SET, PINNED_COMMENTS_SET, COMMENT_MODE_SET } from './types';
|
||||
|
||||
@@ -80,8 +81,11 @@ export const removeComment = (id: Comment['id']): StoreAction<Promise<void>> =>
|
||||
};
|
||||
|
||||
/** fetches comments from server */
|
||||
export const fetchComments = (sort: Sorting): StoreAction<Promise<Tree>> => async dispatch => {
|
||||
export const fetchComments = (sort: Sorting): StoreAction<Promise<Tree>> => async (dispatch, getState) => {
|
||||
const data = await api.getPostComments(sort);
|
||||
const hiddenUsersIds = Object.keys(getState().hiddenUsers);
|
||||
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,
|
||||
|
||||
@@ -37,6 +37,27 @@ function mapTreeIfID(tree: Node[], id: Comment['id'], fn: (c: Node) => Node): No
|
||||
return treeClone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters tree node
|
||||
*/
|
||||
export function filterTree(tree: Node[], fn: (node: Node) => boolean): Node[] {
|
||||
let filtered = false;
|
||||
const newTree = tree.reduce<Node[]>((tree, node) => {
|
||||
if (!fn(node)) {
|
||||
filtered = true;
|
||||
return tree;
|
||||
}
|
||||
const newNode: Node = !node.replies ? node : { ...node, replies: filterTree(node.replies, fn) };
|
||||
if (newNode !== node) {
|
||||
filtered = true;
|
||||
}
|
||||
tree.push(newNode);
|
||||
return tree;
|
||||
}, []);
|
||||
if (!filtered) return tree;
|
||||
return newTree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverses through tree and applies function to comment on which function passed.
|
||||
* Note that function must not mutate comment
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import { createStore, applyMiddleware, AnyAction, compose } from 'redux';
|
||||
import { combineReducers } from 'redux';
|
||||
import thunk, { ThunkAction, ThunkDispatch } from 'redux-thunk';
|
||||
import { Comment, User, PostInfo, Node, BlockedUser, Theme, Sorting, CommentMode } from '@app/common/types';
|
||||
@@ -23,8 +23,10 @@ export interface StoreState {
|
||||
info: PostInfo;
|
||||
/** List of banned users */
|
||||
bannedUsers: BlockedUser[];
|
||||
/** List of hidden users */
|
||||
hiddenUsers: { [id: string]: User };
|
||||
/** Whether list of blocked users should be visible */
|
||||
isBlockedVisible: boolean;
|
||||
isSettingsVisible: boolean;
|
||||
/** Map of collapsed threads */
|
||||
collapsedThreads: {
|
||||
[key: string]: boolean;
|
||||
@@ -41,11 +43,23 @@ const middleware = applyMiddleware(thunk);
|
||||
/**
|
||||
* Thunk Action shortcut
|
||||
*/
|
||||
export type StoreAction<R> = ThunkAction<R, StoreState, undefined, ACTIONS>;
|
||||
export type StoreAction<R, A extends AnyAction = ACTIONS> = ThunkAction<R, StoreState, undefined, A>;
|
||||
|
||||
/**
|
||||
* Thunk Dispatch shortcut
|
||||
*/
|
||||
export type StoreDispatch = ThunkDispatch<StoreState, undefined, ACTIONS>;
|
||||
|
||||
export default createStore(reducers, middleware);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|
||||
: compose;
|
||||
const store = createStore(reducers, composeEnhancers(middleware));
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).ReduxStore = store;
|
||||
}
|
||||
|
||||
export default store;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
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';
|
||||
import thread from './thread/reducers';
|
||||
import sort from './sort/reducers';
|
||||
|
||||
/** Merged store reducers */
|
||||
export default {
|
||||
...sort,
|
||||
...comments,
|
||||
...postinfo,
|
||||
...sort,
|
||||
...theme,
|
||||
...user,
|
||||
...userInfo,
|
||||
|
||||
@@ -3,9 +3,24 @@ import { User, BlockedUser, AuthProvider, BlockTTL } from '@app/common/types';
|
||||
import { ttlToTime } from '@app/utils/ttl-to-time';
|
||||
|
||||
import { StoreAction } from '../index';
|
||||
import { USER_BAN, USER_SET, USER_UNBAN, BLOCKED_VISIBLE_SET, USER_BANLIST_SET } from './types';
|
||||
import {
|
||||
USER_BAN,
|
||||
USER_SET,
|
||||
USER_UNBAN,
|
||||
USER_BANLIST_SET,
|
||||
USER_HIDELIST_SET_ACTION,
|
||||
USER_HIDELIST_SET,
|
||||
USER_HIDE_ACTION,
|
||||
USER_HIDE,
|
||||
USER_UNHIDE_ACTION,
|
||||
USER_UNHIDE,
|
||||
SETTINGS_VISIBLE_SET,
|
||||
} from './types';
|
||||
import { setComments, unsetCommentMode } from '../comments/actions';
|
||||
import { setUserVerified as uSetUserVerified } from '../comments/utils';
|
||||
import { setUserVerified as uSetUserVerified, filterTree } from '../comments/utils';
|
||||
import { IS_STORAGE_AVAILABLE, LS_HIDDEN_USERS_KEY } from '@app/common/constants';
|
||||
import { getItem } from '@app/common/local-storage';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
export const fetchUser = (): StoreAction<Promise<User | null>> => async dispatch => {
|
||||
const user = await api.getUser();
|
||||
@@ -67,7 +82,37 @@ export const unblockUser = (id: User['id']): StoreAction<Promise<void>> => async
|
||||
});
|
||||
};
|
||||
|
||||
export const setVirifiedStatus = (id: User['id'], status: boolean): StoreAction<Promise<void>> => async (
|
||||
export const fetchHiddenUsers = (): StoreAction<void> => dispatch => {
|
||||
if (!IS_STORAGE_AVAILABLE) return;
|
||||
|
||||
const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}');
|
||||
return (dispatch as Dispatch<USER_HIDELIST_SET_ACTION>)({ type: USER_HIDELIST_SET, payload: hiddenUsers });
|
||||
};
|
||||
|
||||
export const hideUser = (user: User): StoreAction<void> => (dispatch, getState) => {
|
||||
if (IS_STORAGE_AVAILABLE) {
|
||||
const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}');
|
||||
hiddenUsers[user.id] = user;
|
||||
localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers));
|
||||
}
|
||||
(dispatch as Dispatch<USER_HIDE_ACTION>)({ type: USER_HIDE, user });
|
||||
|
||||
const comments = getState().comments;
|
||||
return dispatch(setComments(filterTree(comments, node => node.comment.user.id !== user.id)));
|
||||
};
|
||||
|
||||
export const unhideUser = (userId: string): StoreAction<void> => dispatch => {
|
||||
if (IS_STORAGE_AVAILABLE) {
|
||||
const hiddenUsers = JSON.parse(getItem(LS_HIDDEN_USERS_KEY) || '{}');
|
||||
if (hiddenUsers.hasOwnProperty(userId)) {
|
||||
delete hiddenUsers[userId];
|
||||
}
|
||||
localStorage.setItem(LS_HIDDEN_USERS_KEY, JSON.stringify(hiddenUsers));
|
||||
}
|
||||
return (dispatch as Dispatch<USER_UNHIDE_ACTION>)({ type: USER_UNHIDE, id: userId });
|
||||
};
|
||||
|
||||
export const setVerifiedStatus = (id: User['id'], status: boolean): StoreAction<Promise<void>> => async (
|
||||
dispatch,
|
||||
getState
|
||||
) => {
|
||||
@@ -80,9 +125,9 @@ export const setVirifiedStatus = (id: User['id'], status: boolean): StoreAction<
|
||||
dispatch(setComments(uSetUserVerified(comments, id, status)));
|
||||
};
|
||||
|
||||
export const setBlockedVisibleState = (state: boolean): StoreAction<boolean> => dispatch => {
|
||||
export const setSettingsVisibleState = (state: boolean): StoreAction<boolean> => dispatch => {
|
||||
dispatch({
|
||||
type: BLOCKED_VISIBLE_SET,
|
||||
type: SETTINGS_VISIBLE_SET,
|
||||
state,
|
||||
});
|
||||
return state;
|
||||
|
||||
@@ -6,9 +6,12 @@ import {
|
||||
USER_BAN,
|
||||
USER_UNBAN,
|
||||
USER_ACTIONS,
|
||||
BLOCKED_VISIBLE_SET_ACTION,
|
||||
BLOCKED_VISIBLE_SET,
|
||||
SETTINGS_VISIBLE_SET_ACTION,
|
||||
SETTINGS_VISIBLE_SET,
|
||||
USER_BANLIST_SET,
|
||||
USER_HIDELIST_SET,
|
||||
USER_HIDE,
|
||||
USER_UNHIDE,
|
||||
} from './types';
|
||||
|
||||
export const user = (state: StoreState['user'] = null, action: USER_ACTIONS): User | null => {
|
||||
@@ -44,12 +47,31 @@ export const bannedUsers = (state: StoreState['bannedUsers'] = [], action: USER_
|
||||
}
|
||||
};
|
||||
|
||||
export const isBlockedVisible = (
|
||||
state: StoreState['isBlockedVisible'] = false,
|
||||
action: BLOCKED_VISIBLE_SET_ACTION
|
||||
export const hiddenUsers = (state: StoreState['hiddenUsers'] = {}, action: USER_ACTIONS): StoreState['hiddenUsers'] => {
|
||||
switch (action.type) {
|
||||
case USER_HIDELIST_SET: {
|
||||
return action.payload;
|
||||
}
|
||||
case USER_HIDE: {
|
||||
return { ...state, [action.user.id]: action.user };
|
||||
}
|
||||
case USER_UNHIDE: {
|
||||
if (!state.hasOwnProperty(action.id)) return state;
|
||||
const newState = { ...state };
|
||||
delete newState[action.id];
|
||||
return newState;
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export const isSettingsVisible = (
|
||||
state: StoreState['isSettingsVisible'] = false,
|
||||
action: SETTINGS_VISIBLE_SET_ACTION
|
||||
): boolean => {
|
||||
switch (action.type) {
|
||||
case BLOCKED_VISIBLE_SET: {
|
||||
case SETTINGS_VISIBLE_SET: {
|
||||
return action.state;
|
||||
}
|
||||
default:
|
||||
@@ -57,4 +79,4 @@ export const isBlockedVisible = (
|
||||
}
|
||||
};
|
||||
|
||||
export default { user, bannedUsers, isBlockedVisible };
|
||||
export default { user, bannedUsers, hiddenUsers, isSettingsVisible };
|
||||
|
||||
@@ -11,30 +11,48 @@ export interface USER_SET_ACTION {
|
||||
* Set list of banned users
|
||||
*/
|
||||
export const USER_BANLIST_SET = 'USER/BANLIST_SET';
|
||||
|
||||
export interface USER_BANLIST_SET_ACTION {
|
||||
type: typeof USER_BANLIST_SET;
|
||||
list: BlockedUser[];
|
||||
}
|
||||
|
||||
export const USER_BAN = 'USER/BAN';
|
||||
|
||||
export interface USER_BAN_ACTION {
|
||||
type: typeof USER_BAN;
|
||||
user: BlockedUser;
|
||||
}
|
||||
|
||||
export const USER_UNBAN = 'USER/UNBAN';
|
||||
|
||||
export interface USER_UNBAN_ACTION {
|
||||
type: typeof USER_UNBAN;
|
||||
id: User['id'];
|
||||
}
|
||||
|
||||
export const BLOCKED_VISIBLE_SET = 'BLOCKED_VISIBLE/SET';
|
||||
/**
|
||||
* Set list of hidden users
|
||||
*/
|
||||
export const USER_HIDELIST_SET = 'USER/HIDELIST_SET';
|
||||
export interface USER_HIDELIST_SET_ACTION {
|
||||
type: typeof USER_HIDELIST_SET;
|
||||
payload: { [id: string]: User };
|
||||
}
|
||||
|
||||
export interface BLOCKED_VISIBLE_SET_ACTION {
|
||||
type: typeof BLOCKED_VISIBLE_SET;
|
||||
export const USER_HIDE = 'USER/HIDE';
|
||||
export interface USER_HIDE_ACTION {
|
||||
type: typeof USER_HIDE;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const USER_UNHIDE = 'USER/UNHIDE';
|
||||
export interface USER_UNHIDE_ACTION {
|
||||
type: typeof USER_UNHIDE;
|
||||
id: User['id'];
|
||||
}
|
||||
|
||||
export const SETTINGS_VISIBLE_SET = 'SETTINGS_VISIBLE/SET';
|
||||
|
||||
export interface SETTINGS_VISIBLE_SET_ACTION {
|
||||
type: typeof SETTINGS_VISIBLE_SET;
|
||||
state: boolean;
|
||||
}
|
||||
|
||||
@@ -43,4 +61,7 @@ export type USER_ACTIONS =
|
||||
| USER_BANLIST_SET_ACTION
|
||||
| USER_BAN_ACTION
|
||||
| USER_UNBAN_ACTION
|
||||
| BLOCKED_VISIBLE_SET_ACTION;
|
||||
| USER_HIDELIST_SET_ACTION
|
||||
| USER_HIDE_ACTION
|
||||
| USER_UNHIDE_ACTION
|
||||
| SETTINGS_VISIBLE_SET_ACTION;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { StoreAction } from '@app/store';
|
||||
import { Action } from 'redux';
|
||||
|
||||
/** Helper type which is used to convert actionCreator to redux `connect` bound prop */
|
||||
export type BoundActionCreator<A> = A extends (...args: infer U) => StoreAction<infer R>
|
||||
? (...args: U) => R
|
||||
: A extends (...args: infer U) => Action<infer R>
|
||||
? (...args: U) => Action<R>
|
||||
: A extends (...args: infer U) => Promise<infer R>
|
||||
? (...args: U) => Promise<R>
|
||||
: never;
|
||||
|
||||
/** Helper type which is used to convert actionCreators map to redux `connect` bound props */
|
||||
export type BoundActionCreators<T extends object> = { [K in keyof T]: BoundActionCreator<T[K]> };
|
||||
|
||||
/**
|
||||
* no-op function that is used for type conversion for action creators connected
|
||||
* through mapDispatchToProps
|
||||
*/
|
||||
export function bindActions<A extends object>(obj: A): BoundActionCreators<A> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return obj as any;
|
||||
}
|
||||
@@ -1,22 +1,25 @@
|
||||
import { BlockTTL } from '@app/common/types';
|
||||
|
||||
export function ttlToTime(ttl: BlockTTL): string {
|
||||
const now = new Date();
|
||||
if (ttl === 'permanently') {
|
||||
now.setFullYear(now.getFullYear() + 100);
|
||||
return now.toISOString();
|
||||
export function ttlToDate(ttl: BlockTTL): Date {
|
||||
const date = new Date();
|
||||
switch (ttl) {
|
||||
case 'permanently':
|
||||
date.setFullYear(date.getFullYear() + 100);
|
||||
return date;
|
||||
case '43200m':
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
return date;
|
||||
case '10080m':
|
||||
date.setDate(date.getDate() + 7);
|
||||
return date;
|
||||
case '1440m':
|
||||
date.setDate(date.getDate() + 1);
|
||||
return date;
|
||||
default:
|
||||
throw new Error('unknown block ttl');
|
||||
}
|
||||
if (ttl === '43200m') {
|
||||
now.setMonth(now.getMonth() + 1);
|
||||
return now.toISOString();
|
||||
}
|
||||
if (ttl === '10080m') {
|
||||
now.setDate(now.getDate() + 7);
|
||||
return now.toISOString();
|
||||
}
|
||||
if (ttl === '1440m') {
|
||||
now.setDate(now.getDate() + 1);
|
||||
return now.toISOString();
|
||||
}
|
||||
throw new Error('unknown block ttl');
|
||||
}
|
||||
|
||||
export function ttlToTime(ttl: BlockTTL): string {
|
||||
return ttlToDate(ttl).toISOString();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user