Collapse comment by thread border (#545)
* collapse comment by border * Fix after review * hide avatar in collapsed state * fix lines icon
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
.comment__body {
|
||||
+ .comment__user-info {
|
||||
margin-top: 8px;
|
||||
}
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 6px;
|
||||
height: 20px;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 84px;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
line-height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
.comment_collapsed {
|
||||
padding: 12px 0 12px 28px;
|
||||
|
||||
> .comment__body {
|
||||
.comment__text,
|
||||
.comment__actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.comment__info {
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.comment__avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment__username,
|
||||
.comment__time {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.comment__score {
|
||||
top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.comment__body {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.comment__title-link {
|
||||
color: #0e7e9d;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
.comment {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 12px 0 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
@@ -14,3 +12,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment_level_6 {
|
||||
.comment__text,
|
||||
.comment__actions {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,12 +296,6 @@ export class Comment extends Component<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
toggleCollapse = () => {
|
||||
this.props.setReplyEditState!({ id: this.props.data.id, state: CommentMode.None });
|
||||
|
||||
this.props.setCollapse!(this.props.data.id, !this.props.collapsed);
|
||||
};
|
||||
|
||||
copyComment = () => {
|
||||
const username = this.props.data.user.name;
|
||||
const time = this.props.data.time;
|
||||
@@ -514,6 +508,7 @@ export class Comment extends Component<Props, State> {
|
||||
editing: props.view === 'main' && isEditing,
|
||||
theme: props.view === 'preview' ? null : props.theme,
|
||||
level: props.level,
|
||||
collapsed: props.collapsed,
|
||||
};
|
||||
|
||||
if (props.view === 'preview') {
|
||||
@@ -572,96 +567,88 @@ export class Comment extends Component<Props, State> {
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__body">
|
||||
<div className="comment__info">
|
||||
{props.view !== 'user' && <AvatarIcon theme={this.props.theme} picture={o.user.picture} />}
|
||||
<div className="comment__info">
|
||||
{props.view !== 'user' && !props.collapsed && (
|
||||
<AvatarIcon mix="comment__avatar" theme={this.props.theme} picture={o.user.picture} />
|
||||
)}
|
||||
|
||||
{props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleUserInfoVisibility)}
|
||||
className="comment__username"
|
||||
title={o.user.id}
|
||||
>
|
||||
{o.user.name}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...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 })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
||||
<span title="Verified user" className={b('comment__verification', {}, { active: true })} />
|
||||
)}
|
||||
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
|
||||
{o.time}
|
||||
</a>
|
||||
|
||||
{!!props.level && props.level > 0 && props.view === 'main' && (
|
||||
<a
|
||||
className="comment__link-to-parent"
|
||||
href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.pid}`}
|
||||
aria-label="Go to parent comment"
|
||||
title="Go to parent comment"
|
||||
onClick={e => this.scrollToParent(e)}
|
||||
>
|
||||
{' '}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{props.isUserBanned && props.view !== 'user' && <span className="comment__status">Blocked</span>}
|
||||
|
||||
{isAdmin && !props.isUserBanned && props.data.delete && <span className="comment__status">Deleted</span>}
|
||||
|
||||
{!props.disabled && props.view === 'main' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleCollapse)}
|
||||
className={b('comment__action', {}, { type: 'collapse', selected: props.collapsed })}
|
||||
>
|
||||
{props.collapsed ? '+' : '−'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<span className={b('comment__score', {}, { view: o.score.view })}>
|
||||
<span
|
||||
className={b(
|
||||
'comment__vote',
|
||||
{},
|
||||
{ type: 'up', selected: state.scoreDelta === 1, disabled: isUpvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === 1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isUpvotingDisabled ? undefined : this.increaseScore)}
|
||||
title={upvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote up
|
||||
</span>
|
||||
|
||||
<span className="comment__score-value" title={o.controversyText}>
|
||||
{o.score.sign}
|
||||
{o.score.value}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={b(
|
||||
'comment__vote',
|
||||
{},
|
||||
{ type: 'down', selected: state.scoreDelta === -1, disabled: isDownvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === -1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isDownvotingDisabled ? undefined : this.decreaseScore)}
|
||||
title={downvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote down
|
||||
</span>
|
||||
{props.view !== 'user' && (
|
||||
<span
|
||||
{...getHandleClickProps(this.toggleUserInfoVisibility)}
|
||||
className="comment__username"
|
||||
title={o.user.id}
|
||||
>
|
||||
{o.user.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAdmin && props.view !== 'user' && (
|
||||
<span
|
||||
{...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 })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isAdmin && !!o.user.verified && props.view !== 'user' && (
|
||||
<span title="Verified user" className={b('comment__verification', {}, { active: true })} />
|
||||
)}
|
||||
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
|
||||
{o.time}
|
||||
</a>
|
||||
|
||||
{!!props.level && props.level > 0 && props.view === 'main' && (
|
||||
<a
|
||||
className="comment__link-to-parent"
|
||||
href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.pid}`}
|
||||
aria-label="Go to parent comment"
|
||||
title="Go to parent comment"
|
||||
onClick={e => this.scrollToParent(e)}
|
||||
>
|
||||
{' '}
|
||||
</a>
|
||||
)}
|
||||
|
||||
{props.isUserBanned && props.view !== 'user' && <span className="comment__status">Blocked</span>}
|
||||
|
||||
{isAdmin && !props.isUserBanned && props.data.delete && <span className="comment__status">Deleted</span>}
|
||||
|
||||
<span className={b('comment__score', {}, { view: o.score.view })}>
|
||||
<span
|
||||
className={b(
|
||||
'comment__vote',
|
||||
{},
|
||||
{ type: 'up', selected: state.scoreDelta === 1, disabled: isUpvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === 1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isUpvotingDisabled ? undefined : this.increaseScore)}
|
||||
title={upvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote up
|
||||
</span>
|
||||
|
||||
<span className="comment__score-value" title={o.controversyText}>
|
||||
{o.score.sign}
|
||||
{o.score.value}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={b(
|
||||
'comment__vote',
|
||||
{},
|
||||
{ type: 'down', selected: state.scoreDelta === -1, disabled: isDownvotingDisabled }
|
||||
)}
|
||||
aria-disabled={state.scoreDelta === -1 || isUpvotingDisabled ? 'true' : 'false'}
|
||||
{...getHandleClickProps(isDownvotingDisabled ? undefined : this.decreaseScore)}
|
||||
title={downvotingDisabledReason || undefined}
|
||||
>
|
||||
Vote down
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="comment__body">
|
||||
{!!state.voteErrorMessage && (
|
||||
<div className="voting__error" role="alert">
|
||||
Voting error: {state.voteErrorMessage}
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
putVote,
|
||||
setCommentMode,
|
||||
} from '@app/store/comments/actions';
|
||||
import { setCollapse } from '@app/store/thread/actions';
|
||||
import { blockUser, unblockUser, hideUser, setVerifiedStatus } from '@app/store/user/actions';
|
||||
|
||||
import { Comment, Props } from './comment';
|
||||
@@ -65,7 +64,6 @@ export const boundActions = bindActions({
|
||||
updateComment,
|
||||
removeComment,
|
||||
setReplyEditState: setCommentMode,
|
||||
setCollapse,
|
||||
setPinState,
|
||||
putCommentVote: putVote,
|
||||
blockUser,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
.thread__collapse {
|
||||
height: 100%;
|
||||
width: 11px;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
left: -4px;
|
||||
cursor: pointer;
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 5px;
|
||||
top: 0;
|
||||
border-left: 1px dotted #d9d9d9;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
transform: translateX(-1px);
|
||||
border-left: 3px solid #777;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
.thread__collapse_collapsed {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
top: 12px;
|
||||
left: 0;
|
||||
display: flex;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
border-radius: 2px;
|
||||
border: 1px solid;
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.thread__collapse_collapsed > div {
|
||||
position: relative;
|
||||
top: 6px;
|
||||
left: 3px;
|
||||
width: 12px;
|
||||
height: 2px;
|
||||
border-bottom: 2px solid;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
border-bottom: 2px solid;
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 0;
|
||||
}
|
||||
&:after {
|
||||
top: 4px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,11 @@
|
||||
.thread_theme_dark {
|
||||
border-left-color: rgba(255, 255, 255, 0.35);
|
||||
.thread__collapse {
|
||||
&:after {
|
||||
border-color: #505050;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
border-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export { ConnectedThread as Thread } from './thread';
|
||||
export { Thread } from './thread';
|
||||
|
||||
import './thread.scss';
|
||||
import './_theme_dark/thread_theme_dark.scss';
|
||||
|
||||
import './__collapse/thread__collapse.scss';
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
.thread_indented {
|
||||
padding-left: 17px;
|
||||
border-left: 1px dotted rgba(0, 0, 0, 0.15);
|
||||
.thread {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thread_level_6 {
|
||||
border-left: none;
|
||||
padding-left: 0;
|
||||
.thread_indented {
|
||||
margin-left: 17px;
|
||||
}
|
||||
|
||||
.thread_level_6 .thread_level_6 {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@@ -1,26 +1,18 @@
|
||||
/** @jsx createElement */
|
||||
import { createElement, RenderableProps, FunctionComponent } from 'preact';
|
||||
import { useStore } from 'react-redux';
|
||||
import { createElement, FunctionComponent } from 'preact';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import { ConnectedComment as Comment } from '@app/components/comment/connected-comment';
|
||||
import { Comment as CommentInterface } from '@app/common/types';
|
||||
import { getThreadIsCollapsed } from '@app/store/thread/getters';
|
||||
import { getHandleClickProps } from '@app/common/accessibility';
|
||||
import { StoreState } from '@app/store';
|
||||
import { InView } from '../root/in-view/in-view';
|
||||
import { setCollapse } from '@app/store/thread/actions';
|
||||
import { getThreadIsCollapsed } from '@app/store/thread/getters';
|
||||
import { InView } from '@app/components/root/in-view/in-view';
|
||||
import { ConnectedComment as Comment } from '@app/components/comment/connected-comment';
|
||||
|
||||
const mapStateToProps = (state: StoreState, props: { id: CommentInterface['id'] }) => {
|
||||
const comment = state.comments[props.id];
|
||||
return {
|
||||
comment,
|
||||
childs: state.childComments[props.id],
|
||||
collapsed: getThreadIsCollapsed(comment)(state),
|
||||
isCommentsDisabled: !!state.info.read_only,
|
||||
theme: state.theme,
|
||||
};
|
||||
};
|
||||
|
||||
interface OwnProps {
|
||||
interface Props {
|
||||
id: CommentInterface['id'];
|
||||
childs?: CommentInterface['id'][];
|
||||
level: number;
|
||||
@@ -29,10 +21,21 @@ interface OwnProps {
|
||||
getPreview(text: string): Promise<string>;
|
||||
}
|
||||
|
||||
type Props = OwnProps & ReturnType<typeof mapStateToProps>;
|
||||
const commentSelector = (id: string) => (state: StoreState) => {
|
||||
const { theme, comments, childComments } = state;
|
||||
const comment = comments[id];
|
||||
const childs = childComments[id];
|
||||
const collapsed = getThreadIsCollapsed(comment)(state);
|
||||
|
||||
function Thread(props: RenderableProps<Props>) {
|
||||
const { collapsed, comment, childs, level, theme } = props;
|
||||
return { comment, childs, collapsed, theme };
|
||||
};
|
||||
|
||||
export const Thread: FunctionComponent<Props> = ({ id, level, mix, getPreview }) => {
|
||||
const dispatch = useDispatch();
|
||||
const { collapsed, comment, childs, theme } = useSelector(commentSelector(id));
|
||||
const collapse = useCallback(() => {
|
||||
dispatch(setCollapse(id, !collapsed));
|
||||
}, [id, collapsed]);
|
||||
|
||||
if (comment.hidden) return null;
|
||||
|
||||
@@ -41,7 +44,7 @@ function Thread(props: RenderableProps<Props>) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={b('thread', props, { level, theme, indented })}
|
||||
className={b('thread', { mix }, { level, theme, indented })}
|
||||
role={['listitem'].concat(!collapsed && !!repliesCount ? 'list' : []).join(' ')}
|
||||
aria-expanded={!collapsed}
|
||||
>
|
||||
@@ -49,7 +52,7 @@ function Thread(props: RenderableProps<Props>) {
|
||||
{inviewProps => (
|
||||
<Comment
|
||||
ref={ref => inviewProps.ref(ref)}
|
||||
key={`comment-${props.id}`}
|
||||
key={`comment-${id}`}
|
||||
view="main"
|
||||
data={comment}
|
||||
repliesCount={repliesCount}
|
||||
@@ -62,14 +65,18 @@ function Thread(props: RenderableProps<Props>) {
|
||||
{!collapsed &&
|
||||
childs &&
|
||||
!!childs.length &&
|
||||
childs.map(id => (
|
||||
<ConnectedThread key={`thread-${id}`} id={id} level={Math.min(level + 1, 6)} getPreview={props.getPreview} />
|
||||
childs.map(currentId => (
|
||||
<Thread key={`thread-${currentId}`} id={currentId} level={Math.min(level + 1, 6)} getPreview={getPreview} />
|
||||
))}
|
||||
{level < 6 && (
|
||||
<div
|
||||
className={b('thread__collapse', { mods: { collapsed } })}
|
||||
role="button"
|
||||
{...getHandleClickProps(collapse)}
|
||||
>
|
||||
<div></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const ConnectedThread: FunctionComponent<OwnProps> = props => {
|
||||
const providedProps = mapStateToProps(useStore().getState(), props);
|
||||
return <Thread {...props} {...providedProps} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user