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:
Pavel Mineev
2020-01-14 17:11:30 -06:00
committed by Umputun
parent d0ddd8aa80
commit ce01a347ca
12 changed files with 244 additions and 139 deletions
@@ -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;
+7 -2
View File
@@ -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;
}
}
+80 -93
View File
@@ -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;
}
}
}
+3 -1
View File
@@ -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';
+9 -6
View File
@@ -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;
}
+36 -29
View File
@@ -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} />;
};