mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 10:56:49 +08:00
Implement Stream TabBar
This commit is contained in:
@@ -47,3 +47,7 @@ export const removeCommentClassName = (idx) => ({
|
||||
type: actions.REMOVE_COMMENT_CLASSNAME,
|
||||
idx
|
||||
});
|
||||
|
||||
export const setActiveTab = (tab) => (dispatch) => {
|
||||
dispatch({type: actions.SET_ACTIVE_TAB, tab});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import React from 'react';
|
||||
|
||||
import LoadMore from './LoadMore';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import NewCount from './NewCount';
|
||||
import {TransitionGroup} from 'react-transition-group';
|
||||
import {forEachError} from 'coral-framework/utils';
|
||||
import Comment from '../components/Comment';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second comment of
|
||||
// the current comment list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
const comments = props.root.asset.comments;
|
||||
if (comments && comments.nodes.length) {
|
||||
const idCursors = [comments.nodes[0].id];
|
||||
if (comments.nodes[1]) {
|
||||
idCursors.push(comments.nodes[1].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
// by one of the 2 id cursors. It returns a new set of id cursors calculated
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const comments = props.root.asset.comments;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
|
||||
const nextInLine = comments.nodes[index + 1];
|
||||
if (nextInLine) {
|
||||
idCursors.push(nextInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
|
||||
class AllCommentsPane extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...resetCursors(this.state, props),
|
||||
loadingState: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {comments: prevComments} = this.props;
|
||||
const {comments: nextComments} = next;
|
||||
|
||||
if (!prevComments && nextComments) {
|
||||
this.setState(resetCursors);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.nodes.length < prevComments.nodes.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadMore = () => {
|
||||
this.setState({loadingState: 'loading'});
|
||||
this.props.loadMore()
|
||||
.then(() => {
|
||||
this.setState({loadingState: 'success'});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
});
|
||||
}
|
||||
|
||||
viewNewComments = () => {
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which were authored by current user or comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
const {comments, currentUser: user} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = user ? user.id : null;
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.nodes.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor || comment.user.id === userId) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
comments,
|
||||
commentClassNames,
|
||||
addTag,
|
||||
removeTag,
|
||||
ignoreUser,
|
||||
setActiveReplyBox,
|
||||
activeReplyBox,
|
||||
addNotification,
|
||||
disableReply,
|
||||
postComment,
|
||||
asset,
|
||||
currentUser,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
loadNewReplies,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
commentIsIgnored,
|
||||
charCountEnable,
|
||||
maxCharCount,
|
||||
editComment,
|
||||
} = this.props;
|
||||
|
||||
const {loadingState} = this.state;
|
||||
const view = this.getVisibleComments();
|
||||
|
||||
return (
|
||||
<div className="talk-stream-comments-container">
|
||||
<NewCount
|
||||
count={comments.nodes.length - view.length}
|
||||
loadMore={this.viewNewComments}
|
||||
/>
|
||||
<TransitionGroup component='div' className="embed__stream">
|
||||
{view.map((comment) => {
|
||||
return commentIsIgnored(comment)
|
||||
? <IgnoredCommentTombstone key={comment.id} />
|
||||
: <Comment
|
||||
commentClassNames={commentClassNames}
|
||||
data={data}
|
||||
root={root}
|
||||
disableReply={disableReply}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={currentUser}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
editComment={editComment}
|
||||
/>;
|
||||
})}
|
||||
</TransitionGroup>
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
moreComments={asset.comments.hasNextPage}
|
||||
loadMore={this.loadMore}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default AllCommentsPane;
|
||||
@@ -1,8 +1,17 @@
|
||||
.root {
|
||||
margin-top: 16px;
|
||||
margin-left: 20px;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.rootLevel0:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.root:first-child {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.rootLevel0 {
|
||||
@@ -52,6 +61,13 @@
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* element in the top right of the Comment */
|
||||
.topRight {
|
||||
float: right;
|
||||
|
||||
@@ -134,7 +134,6 @@ export default class Comment extends React.Component {
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
reactKey: PropTypes.string.isRequired,
|
||||
|
||||
// id of currently opened ReplyBox. tracked in Stream.js
|
||||
activeReplyBox: PropTypes.string.isRequired,
|
||||
@@ -148,12 +147,8 @@ export default class Comment extends React.Component {
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
postComment: PropTypes.func.isRequired,
|
||||
depth: PropTypes.number.isRequired,
|
||||
liveUpdates: PropTypes.bool.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
url: PropTypes.string
|
||||
}).isRequired,
|
||||
liveUpdates: PropTypes.bool,
|
||||
asset: PropTypes.object.isRequired,
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired
|
||||
}),
|
||||
@@ -335,7 +330,6 @@ export default class Comment extends React.Component {
|
||||
|
||||
const view = this.getVisibileReplies();
|
||||
const {loadingState} = this.state;
|
||||
const isReply = !!parentId;
|
||||
const isPending = comment.id.indexOf('pending') >= 0;
|
||||
const isHighlighted = highlighted === comment.id;
|
||||
|
||||
@@ -372,7 +366,7 @@ export default class Comment extends React.Component {
|
||||
addTag({
|
||||
id: comment.id,
|
||||
name: BEST_TAG,
|
||||
assetId: asset.id
|
||||
assetId: asset.id,
|
||||
}),
|
||||
() => 'Failed to tag comment as best'
|
||||
);
|
||||
@@ -382,7 +376,7 @@ export default class Comment extends React.Component {
|
||||
removeTag({
|
||||
id: comment.id,
|
||||
name: BEST_TAG,
|
||||
assetId: asset.id
|
||||
assetId: asset.id,
|
||||
}),
|
||||
() => 'Failed to remove best comment tag'
|
||||
);
|
||||
@@ -422,7 +416,6 @@ export default class Comment extends React.Component {
|
||||
className={cn(...rootClassNames)}
|
||||
id={`c_${comment.id}`}
|
||||
>
|
||||
{!isReply && <hr aria-hidden={true} />}
|
||||
<div
|
||||
className={cn(
|
||||
'talk-stream-comment',
|
||||
@@ -491,9 +484,9 @@ export default class Comment extends React.Component {
|
||||
? <EditableCommentContent
|
||||
editComment={this.editComment}
|
||||
addNotification={addNotification}
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
currentUser={currentUser}
|
||||
charCountEnable={charCountEnable}
|
||||
maxCharCount={maxCharCount}
|
||||
parentId={parentId}
|
||||
stopEditing={this.stopEditing}
|
||||
@@ -602,7 +595,6 @@ export default class Comment extends React.Component {
|
||||
showSignInDialog={showSignInDialog}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
liveUpdates={liveUpdates}
|
||||
reactKey={reply.id}
|
||||
key={reply.id}
|
||||
comment={reply}
|
||||
/>;
|
||||
|
||||
@@ -18,11 +18,6 @@ export class EditableCommentContent extends React.Component {
|
||||
|
||||
// show notification to the user (e.g. for errors)
|
||||
addNotification: PropTypes.func.isRequired,
|
||||
asset: PropTypes.shape({
|
||||
settings: PropTypes.shape({
|
||||
charCountEnable: PropTypes.bool,
|
||||
}),
|
||||
}).isRequired,
|
||||
|
||||
// comment that is being edited
|
||||
comment: PropTypes.shape({
|
||||
@@ -39,6 +34,7 @@ export class EditableCommentContent extends React.Component {
|
||||
currentUser: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired
|
||||
}),
|
||||
charCountEnable: PropTypes.bool,
|
||||
maxCharCount: PropTypes.number,
|
||||
|
||||
// edit a comment, passed {{ body }}
|
||||
@@ -121,7 +117,7 @@ export class EditableCommentContent extends React.Component {
|
||||
<div className={styles.editCommentForm}>
|
||||
<CommentForm
|
||||
defaultValue={this.props.comment.body}
|
||||
charCountEnable={this.props.asset.settings.charCountEnable}
|
||||
charCountEnable={this.props.charCountEnable}
|
||||
maxCharCount={this.props.maxCharCount}
|
||||
submitEnabled={this.isSubmitEnabled}
|
||||
body={this.state.body}
|
||||
|
||||
@@ -4,65 +4,66 @@ import Slot from 'coral-framework/components/Slot';
|
||||
import {can} from 'coral-framework/services/perms';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import {TabBar, Tab, TabContent, Button} from 'coral-ui';
|
||||
import Count from 'coral-plugin-comment-count/CommentCount';
|
||||
import {TabBar, Tab, TabContent, TabPane} from 'coral-ui';
|
||||
import ProfileContainer from 'coral-settings/containers/ProfileContainer';
|
||||
import ConfigureStreamContainer
|
||||
from 'coral-configure/containers/ConfigureStreamContainer';
|
||||
|
||||
export default class Embed extends React.Component {
|
||||
changeTab = (tab) => {
|
||||
switch (tab) {
|
||||
case 0:
|
||||
this.props.setActiveTab('stream');
|
||||
break;
|
||||
case 1:
|
||||
this.props.setActiveTab('profile');
|
||||
|
||||
// TODO: move data fetching to profile container.
|
||||
// TODO: move data fetching to appropiate containers.
|
||||
switch (tab) {
|
||||
case 'profile':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
case 2:
|
||||
this.props.setActiveTab('config');
|
||||
|
||||
// TODO: move data fetching to config container.
|
||||
case 'config':
|
||||
this.props.data.refetch();
|
||||
break;
|
||||
}
|
||||
this.props.setActiveTab(tab);
|
||||
};
|
||||
|
||||
handleShowProfile = () => this.props.setActiveTab('profile');
|
||||
|
||||
render() {
|
||||
const {activeTab, viewAllComments, commentId} = this.props;
|
||||
const {asset: {totalCommentCount}} = this.props.root;
|
||||
const {activeTab} = this.props;
|
||||
const {user} = this.props.auth;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="commentStream">
|
||||
<TabBar onChange={this.changeTab} activeTab={activeTab} className='talk-stream-tabbar'>
|
||||
<Tab className={'talk-stream-comment-count-tab'} id='stream'><Count count={totalCommentCount}/></Tab>
|
||||
<Tab className={'talk-stream-profile-tab'} id='profile'>{t('framework.my_profile')}</Tab>
|
||||
<Tab className={'talk-stream-configuration-tab'} id='config' restricted={!can(user, 'UPDATE_CONFIG')}>{t('framework.configure_stream')}</Tab>
|
||||
<TabBar
|
||||
onTabClick={this.changeTab}
|
||||
activeTab={activeTab}
|
||||
className='talk-stream-tabbar'
|
||||
aria-controls='talk-embed-tab'
|
||||
>
|
||||
<Tab tabId={'stream'} className={'talk-stream-comment-count-tab'}>
|
||||
{t('embed_comments_tab')}
|
||||
</Tab>
|
||||
<Tab tabId={'profile'} className={'talk-stream-profile-tab'}>
|
||||
{t('framework.my_profile')}
|
||||
</Tab>
|
||||
{can(user, 'UPDATE_CONFIG') &&
|
||||
<Tab tabId={'config'} className={'talk-stream-configuration-tab'}>
|
||||
{t('framework.configure_stream')}
|
||||
</Tab>
|
||||
}
|
||||
</TabBar>
|
||||
{commentId &&
|
||||
<Button
|
||||
cStyle="darkGrey"
|
||||
style={{float: 'right'}}
|
||||
onClick={viewAllComments}
|
||||
>
|
||||
{t('framework.show_all_comments')}
|
||||
</Button>}
|
||||
<Slot fill="embed" />
|
||||
<TabContent show={activeTab === 'stream'}>
|
||||
<Stream data={this.props.data} root={this.props.root} />
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 'profile'}>
|
||||
<ProfileContainer />
|
||||
</TabContent>
|
||||
<TabContent show={activeTab === 'config'}>
|
||||
<ConfigureStreamContainer />
|
||||
|
||||
<TabContent
|
||||
activeTab={activeTab}
|
||||
id='talk-embed-tab-content'
|
||||
>
|
||||
<TabPane tabId={'stream'}>
|
||||
<Stream data={this.props.data} root={this.props.root} />
|
||||
</TabPane>
|
||||
<TabPane tabId={'profile'}>
|
||||
<ProfileContainer />
|
||||
</TabPane>
|
||||
<TabPane tabId={'config'}>
|
||||
<ConfigureStreamContainer />
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.root {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.viewAllButton {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.tabPanel {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
import LoadMore from './LoadMore';
|
||||
import {StreamError} from './StreamError';
|
||||
import Comment from '../components/Comment';
|
||||
import SuspendedAccount from './SuspendedAccount';
|
||||
@@ -12,161 +11,71 @@ import RestrictedMessageBox
|
||||
import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import CommentBox from 'coral-plugin-commentbox/CommentBox';
|
||||
import QuestionBox from 'coral-plugin-questionbox/QuestionBox';
|
||||
import IgnoredCommentTombstone from './IgnoredCommentTombstone';
|
||||
import NewCount from './NewCount';
|
||||
import {TransitionGroup} from 'react-transition-group';
|
||||
import {forEachError} from 'coral-framework/utils';
|
||||
import {Button, TabBar, Tab, TabCount, TabContent, TabPane} from 'coral-ui';
|
||||
|
||||
import {getTopLevelParent} from '../graphql/utils';
|
||||
import AllCommentsPane from './AllCommentsPane';
|
||||
|
||||
const hasComment = (nodes, id) => nodes.some((node) => node.id === id);
|
||||
|
||||
// resetCursors will return the id cursors of the first and second comment of
|
||||
// the current comment list. The cursors are used to dertermine which
|
||||
// comments to show. The spare cursor functions as a backup in case one
|
||||
// of the comments gets deleted.
|
||||
function resetCursors(state, props) {
|
||||
const comments = props.root.asset.comments;
|
||||
if (comments && comments.nodes.length) {
|
||||
const idCursors = [comments.nodes[0].id];
|
||||
if (comments.nodes[1]) {
|
||||
idCursors.push(comments.nodes[1].id);
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
return {idCursors: []};
|
||||
}
|
||||
|
||||
// invalidateCursor is called whenever a comment is removed which is referenced
|
||||
// by one of the 2 id cursors. It returns a new set of id cursors calculated
|
||||
// using the help of the backup cursor.
|
||||
function invalidateCursor(invalidated, state, props) {
|
||||
const alt = invalidated === 1 ? 0 : 1;
|
||||
const comments = props.root.asset.comments;
|
||||
const idCursors = [];
|
||||
if (state.idCursors[alt]) {
|
||||
idCursors.push(state.idCursors[alt]);
|
||||
const index = comments.nodes.findIndex((node) => node.id === idCursors[0]);
|
||||
const nextInLine = comments.nodes[index + 1];
|
||||
if (nextInLine) {
|
||||
idCursors.push(nextInLine.id);
|
||||
}
|
||||
}
|
||||
return {idCursors};
|
||||
}
|
||||
import styles from './Stream.css';
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
...resetCursors(this.state, props),
|
||||
keepCommentBox: false,
|
||||
loadingState: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(next) {
|
||||
const {root: {asset: {comments: prevComments}}} = this.props;
|
||||
const {root: {asset: {comments: nextComments}}} = next;
|
||||
|
||||
if (!prevComments && nextComments) {
|
||||
this.setState(resetCursors);
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep comment box when user was live suspended, banned, ...
|
||||
if (!this.userIsDegraged(this.props) && this.userIsDegraged(next)) {
|
||||
this.setState({keepCommentBox: true});
|
||||
}
|
||||
|
||||
if (
|
||||
prevComments && nextComments &&
|
||||
nextComments.nodes.length < prevComments.nodes.length
|
||||
) {
|
||||
|
||||
// Invalidate first cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[0] && !hasComment(nextComments.nodes, this.state.idCursors[0])) {
|
||||
this.setState(invalidateCursor(0, this.state, next));
|
||||
}
|
||||
|
||||
// Invalidate second cursor if referenced comment was removed.
|
||||
if (this.state.idCursors[1] && !hasComment(nextComments.nodes, this.state.idCursors[1])) {
|
||||
this.setState(invalidateCursor(1, this.state, next));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewNewComments = () => {
|
||||
this.setState(resetCursors);
|
||||
};
|
||||
|
||||
setActiveReplyBox = (reactKey) => {
|
||||
setActiveReplyBox = (id) => {
|
||||
if (!this.props.auth.user) {
|
||||
this.props.showSignInDialog();
|
||||
} else {
|
||||
this.props.setActiveReplyBox(reactKey);
|
||||
this.props.setActiveReplyBox(id);
|
||||
}
|
||||
};
|
||||
|
||||
loadMoreComments = () => {
|
||||
this.setState({loadingState: 'loading'});
|
||||
this.props.loadMoreComments()
|
||||
.then(() => {
|
||||
this.setState({loadingState: 'success'});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({loadingState: 'error'});
|
||||
forEachError(error, ({msg}) => {this.props.addNotification('error', msg);});
|
||||
});
|
||||
}
|
||||
|
||||
// getVisibileComments returns a list containing comments
|
||||
// which were authored by current user or comes after the `idCursor`.
|
||||
getVisibleComments() {
|
||||
const {root: {asset: {comments}}, auth: {user}} = this.props;
|
||||
const idCursor = this.state.idCursors[0];
|
||||
const userId = user ? user.id : null;
|
||||
|
||||
if (!comments) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const view = [];
|
||||
let pastCursor = false;
|
||||
comments.nodes.forEach((comment) => {
|
||||
if (comment.id === idCursor) {
|
||||
pastCursor = true;
|
||||
}
|
||||
if (pastCursor || comment.user.id === userId) {
|
||||
view.push(comment);
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
userIsDegraged({auth: {user}} = this.props) {
|
||||
return !can(user, 'INTERACT_WITH_COMMUNITY');
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
appendItemArray,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comments}, comment, me},
|
||||
root: {asset, asset: {comments, totalCommentCount}, comment, me},
|
||||
postComment,
|
||||
addNotification,
|
||||
editComment,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
updateItem,
|
||||
addTag,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
viewAllComments,
|
||||
auth: {loggedIn, user},
|
||||
removeTag,
|
||||
pluginProps,
|
||||
editName
|
||||
} = this.props;
|
||||
const {keepCommentBox, loadingState} = this.state;
|
||||
const view = this.getVisibleComments();
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = asset.closedAt === null;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
@@ -194,7 +103,15 @@ class Stream extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="stream">
|
||||
<div id="stream" className={styles.root}>
|
||||
{comment &&
|
||||
<Button
|
||||
cStyle="darkGrey"
|
||||
className={styles.viewAllButton}
|
||||
onClick={viewAllComments}
|
||||
>
|
||||
{t('framework.show_all_comments')}
|
||||
</Button>}
|
||||
|
||||
{open
|
||||
? <div id="commentBox">
|
||||
@@ -211,7 +128,7 @@ class Stream extends React.Component {
|
||||
<RestrictedMessageBox>
|
||||
{t(
|
||||
'stream.temporarily_suspended',
|
||||
this.props.root.settings.organizationName,
|
||||
root.settings.organizationName,
|
||||
timeago(user.suspension.until)
|
||||
)}
|
||||
</RestrictedMessageBox>}
|
||||
@@ -223,10 +140,10 @@ class Stream extends React.Component {
|
||||
/>}
|
||||
{showCommentBox &&
|
||||
<CommentBox
|
||||
addNotification={this.props.addNotification}
|
||||
postComment={this.props.postComment}
|
||||
appendItemArray={this.props.appendItemArray}
|
||||
updateItem={this.props.updateItem}
|
||||
addNotification={addNotification}
|
||||
postComment={postComment}
|
||||
appendItemArray={appendItemArray}
|
||||
updateItem={updateItem}
|
||||
assetId={asset.id}
|
||||
premod={asset.settings.moderation}
|
||||
isReply={false}
|
||||
@@ -253,79 +170,78 @@ class Stream extends React.Component {
|
||||
{/* the highlightedComment is isolated after the user followed a permalink */}
|
||||
{highlightedComment
|
||||
? <Comment
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={this.props.postComment}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postFlag={this.props.postFlag}
|
||||
postDontAgree={this.props.postDontAgree}
|
||||
loadMore={this.props.loadNewReplies}
|
||||
deleteAction={this.props.deleteAction}
|
||||
showSignInDialog={this.props.showSignInDialog}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={highlightedComment.id}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
reactKey={highlightedComment.id}
|
||||
comment={highlightedComment}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={this.props.editComment}
|
||||
editComment={editComment}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
: <div className="talk-stream-comments-container">
|
||||
<NewCount
|
||||
count={comments.nodes.length - view.length}
|
||||
loadMore={this.viewNewComments}
|
||||
/>
|
||||
<TransitionGroup component='div' className="embed__stream">
|
||||
{view.map((comment) => {
|
||||
return commentIsIgnored(comment)
|
||||
? <IgnoredCommentTombstone key={comment.id} />
|
||||
: <Comment
|
||||
commentClassNames={commentClassNames}
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
disableReply={!open}
|
||||
setActiveReplyBox={this.setActiveReplyBox}
|
||||
activeReplyBox={this.props.activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
loadMore={this.props.loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={comment.id}
|
||||
reactKey={comment.id}
|
||||
comment={comment}
|
||||
pluginProps={pluginProps}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={this.props.editComment}
|
||||
liveUpdates={false}
|
||||
/>;
|
||||
})}
|
||||
</TransitionGroup>
|
||||
<LoadMore
|
||||
topLevel={true}
|
||||
moreComments={asset.comments.hasNextPage}
|
||||
loadMore={this.loadMoreComments}
|
||||
loadingState={loadingState}
|
||||
/>
|
||||
</div>}
|
||||
: <div>
|
||||
<TabBar activeTab={activeStreamTab} onTabClick={setActiveStreamTab} sub>
|
||||
<Tab tabId={'featured'}>
|
||||
Featured
|
||||
</Tab>
|
||||
<Tab tabId={'all'}>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
</TabBar>
|
||||
<TabContent activeTab={activeStreamTab} sub>
|
||||
<TabPane tabId={'featured'}>
|
||||
TODO
|
||||
</TabPane>
|
||||
<TabPane tabId={'all'}>
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
addTag={addTag}
|
||||
removeTag={removeTag}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadMoreComments}
|
||||
loadNewReplies={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
commentIsIgnored={commentIsIgnored}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
/>
|
||||
</TabPane>
|
||||
</TabContent>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ export const VIEW_ALL_COMMENTS = 'VIEW_ALL_COMMENTS';
|
||||
export const ADD_COMMENT_CLASSNAME = 'ADD_COMMENT_CLASSNAME';
|
||||
export const REMOVE_COMMENT_CLASSNAME = 'REMOVE_COMMENT_CLASSNAME';
|
||||
export const THREADING_LEVEL = process.env.TALK_THREADING_LEVEL;
|
||||
export const SET_ACTIVE_TAB = 'CORAL_STREAM_SET_ACTIVE_TAB';
|
||||
|
||||
@@ -16,7 +16,6 @@ import {addNotification} from 'coral-framework/actions/notification';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import {setActiveTab} from '../actions/embed';
|
||||
import {viewAllComments} from '../actions/stream';
|
||||
|
||||
const {logout, checkLogin} = authActions;
|
||||
const {fetchAssetSuccess} = assetActions;
|
||||
@@ -185,7 +184,6 @@ const mapDispatchToProps = (dispatch) =>
|
||||
logout,
|
||||
checkLogin,
|
||||
setActiveTab,
|
||||
viewAllComments,
|
||||
fetchAssetSuccess,
|
||||
addNotification,
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
import {notificationActions, authActions} from 'coral-framework';
|
||||
import {editName} from 'coral-framework/actions/user';
|
||||
import {setActiveReplyBox} from '../actions/stream';
|
||||
import {setActiveReplyBox, setActiveTab, viewAllComments} from '../actions/stream';
|
||||
import Stream from '../components/Stream';
|
||||
import Comment from './Comment';
|
||||
import {withFragments} from 'coral-framework/hocs';
|
||||
@@ -308,6 +308,8 @@ const mapStateToProps = (state) => ({
|
||||
assetUrl: state.stream.assetUrl,
|
||||
activeTab: state.embed.activeTab,
|
||||
previousTab: state.embed.previousTab,
|
||||
activeStreamTab: state.stream.activeTab,
|
||||
previousStreamTab: state.stream.previousTab,
|
||||
commentClassNames: state.stream.commentClassNames
|
||||
});
|
||||
|
||||
@@ -317,6 +319,8 @@ const mapDispatchToProps = (dispatch) =>
|
||||
addNotification,
|
||||
setActiveReplyBox,
|
||||
editName,
|
||||
viewAllComments,
|
||||
setActiveStreamTab: setActiveTab,
|
||||
}, dispatch);
|
||||
|
||||
export default compose(
|
||||
|
||||
@@ -20,11 +20,19 @@ const initialState = {
|
||||
assetId: getQueryVariable('asset_id'),
|
||||
assetUrl: getQueryVariable('asset_url'),
|
||||
commentId: getQueryVariable('comment_id'),
|
||||
commentClassNames: []
|
||||
commentClassNames: [],
|
||||
activeTab: 'all',
|
||||
previousTab: '',
|
||||
};
|
||||
|
||||
export default function stream(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.SET_ACTIVE_TAB:
|
||||
return {
|
||||
...state,
|
||||
activeTab: action.tab,
|
||||
previousTab: state.activeTab,
|
||||
};
|
||||
case authActions.LOGOUT:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -25,24 +25,24 @@ body {
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 5px 0px 5px 0px;
|
||||
.coralButton {
|
||||
margin: 5px 10px 5px 0px;
|
||||
background: none;
|
||||
padding: 0px;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
.coralButton:hover {
|
||||
border-radius: 2px;
|
||||
color: #767676;
|
||||
}
|
||||
|
||||
button i {
|
||||
.coralButton i {
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
hr {
|
||||
.coralHr {
|
||||
border: 0;
|
||||
height: 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
.buttonReset {
|
||||
|
||||
/* reset button */
|
||||
user-select: none;
|
||||
outline: invert none medium;
|
||||
border: none;
|
||||
touch-action: manipulation;
|
||||
padding: 0;
|
||||
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
/* Unify anchor and button. */
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
align-items: flex-start;
|
||||
vertical-align: middle;
|
||||
whiteSpace: nowrap;
|
||||
background: transparent;
|
||||
font-size: inherit;
|
||||
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0) !important;
|
||||
&::-moz-focus-inner: {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
margin: 5px 10px 5px 0px;
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import React, {Component, PropTypes} from 'react';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import {Icon} from 'coral-ui';
|
||||
import classnames from 'classnames';
|
||||
import cn from 'classnames';
|
||||
import styles from './BestButton.css';
|
||||
|
||||
// tag string for best comments
|
||||
export const BEST_TAG = 'BEST';
|
||||
@@ -95,7 +96,7 @@ export class BestButton extends Component {
|
||||
return (
|
||||
<button onClick={isBest ? this.onClickRemoveBest : this.onClickAddBest}
|
||||
disabled={disabled}
|
||||
className={classnames(`${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)}
|
||||
className={cn(styles.button, `${name}-button`, `e2e__${isBest ? 'unset' : 'set'}-best-comment`)}
|
||||
aria-label={t(isBest ? 'unset_best' : 'set_best')}>
|
||||
<Icon name={ isBest ? 'star' : 'star_border' } />
|
||||
</button>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React, {PropTypes} from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
const name = 'coral-plugin-comment-count';
|
||||
|
||||
const CommentCount = ({count}) => {
|
||||
return <div className={`${name}-text`}>
|
||||
{`${count} ${count === 1 ? t('comment_singular') : t('comment_plural')}`}
|
||||
</div>;
|
||||
};
|
||||
|
||||
CommentCount.propTypes = {
|
||||
count: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default CommentCount;
|
||||
@@ -1,4 +1,5 @@
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
margin: 5px 10px 5px 0px;
|
||||
}
|
||||
@@ -2,15 +2,17 @@ import React, {PropTypes} from 'react';
|
||||
|
||||
import t from 'coral-framework/services/i18n';
|
||||
|
||||
import classnames from 'classnames';
|
||||
import styles from './ReplyButton.css';
|
||||
import cn from 'classnames';
|
||||
|
||||
const name = 'coral-plugin-replies';
|
||||
|
||||
const ReplyButton = ({onClick}) => {
|
||||
return (
|
||||
<button
|
||||
className={classnames(`${name}-reply-button`)}
|
||||
onClick={onClick}>
|
||||
className={cn(`${name}-reply-button`, styles.button)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{t('reply')}
|
||||
<i className={`${name}-icon material-icons`}
|
||||
aria-hidden={true}>reply</i>
|
||||
|
||||
@@ -1,9 +1,64 @@
|
||||
li.base--active {
|
||||
background: white;
|
||||
font-weight: bold;
|
||||
.root {
|
||||
display: inline-block;
|
||||
margin-right: -1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
li.material--active {
|
||||
font-weight: bold;
|
||||
border-bottom: solid 2px black;
|
||||
.rootActive {
|
||||
|
||||
}
|
||||
|
||||
.rootSub {
|
||||
display: inline-block;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
|
||||
.rootSubActive {
|
||||
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
padding: 8px 10px;
|
||||
color: #4E5259;
|
||||
border: solid 1px #D8D8D8;
|
||||
background: #F0F0F0;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:hover, .button:focus {
|
||||
background: #d5d5d5;
|
||||
border-bottom: 1px solid #d5d5d5;
|
||||
}
|
||||
|
||||
.buttonActive, .buttonActive:hover, .buttonActive:focus {
|
||||
background: white;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid white;
|
||||
}
|
||||
|
||||
.buttonSub {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
color: black;
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 6px 12px;
|
||||
margin-bottom: 3px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.buttonSub:hover, .buttonSub:focus {
|
||||
background: transparent;
|
||||
border-bottom: solid 3px #d5d5d5;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.buttonSubActive, .buttonSubActive:hover, .buttonSubActive:focus {
|
||||
font-weight: bold;
|
||||
border-bottom: solid 3px #10589b;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,88 @@
|
||||
import React from 'react';
|
||||
import styles from './Tab.css';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default ({children, tabId, active, onTabClick, cStyle = 'base', ...props}) => (
|
||||
<li
|
||||
key={tabId}
|
||||
className={`${active ? `${styles[`${cStyle}--active`]} talk-tab-active` : ''} talk-tab ${props.className}`}
|
||||
onClick={() => onTabClick(tabId)}
|
||||
>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
class Tab extends React.Component {
|
||||
handleTabClick = () => {
|
||||
if (this.props.onTabClick) {
|
||||
this.props.onTabClick(this.props.tabId);
|
||||
}
|
||||
}
|
||||
|
||||
getRootClassName({active, className, sub, classNames = {}} = this.props) {
|
||||
return cn(
|
||||
'talk-tab',
|
||||
className,
|
||||
{
|
||||
[classNames.root || styles.root]: !sub,
|
||||
[classNames.rootSub || styles.rootSub]: sub,
|
||||
[classNames.rootActive || styles.rootActive]: active && !sub,
|
||||
[classNames.rootSubActive || styles.rootSubActive]: active && sub,
|
||||
'talk-tab-active': active,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getButtonClassName({sub, active, classNames = {}} = this.props) {
|
||||
return cn(
|
||||
'talk-tab-button',
|
||||
{
|
||||
[classNames.button || styles.button]: !sub,
|
||||
[classNames.buttonSub || styles.buttonSub]: sub,
|
||||
[classNames.buttonActive || styles.buttonActive]: active && !sub,
|
||||
[classNames.buttonSubActive || styles.buttonSubActive]: active && sub,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
classNames: _a,
|
||||
active,
|
||||
onTabClick: _c,
|
||||
tabId: _d,
|
||||
sub: _e,
|
||||
'aria-controls': ariaControls,
|
||||
...rest,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<li
|
||||
{...rest}
|
||||
role="presentation"
|
||||
className={this.getRootClassName()}
|
||||
>
|
||||
<button
|
||||
aria-controls={ariaControls}
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
className={this.getButtonClassName()}
|
||||
onClick={this.handleTabClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Tab.propTypes = {
|
||||
className: PropTypes.string,
|
||||
classNames: PropTypes.shape({
|
||||
root: PropTypes.string,
|
||||
rootActive: PropTypes.string,
|
||||
rootSub: PropTypes.string,
|
||||
rootSubActive: PropTypes.string,
|
||||
button: PropTypes.string,
|
||||
buttonActive: PropTypes.string,
|
||||
buttonSub: PropTypes.string,
|
||||
buttonSubActive: PropTypes.string,
|
||||
}),
|
||||
active: PropTypes.bool,
|
||||
onTabClick: PropTypes.func,
|
||||
sub: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Tab;
|
||||
|
||||
@@ -1,44 +1,14 @@
|
||||
.base {
|
||||
.root {
|
||||
list-style: none;
|
||||
border-bottom: solid 1px #D8D8D8;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.base li {
|
||||
color: #4E5259;
|
||||
border: solid 1px #D8D8D8;
|
||||
background: #F0F0F0;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
display: inline-block;
|
||||
border-bottom: none;
|
||||
padding: 8px 10px;
|
||||
margin-right: -1px;
|
||||
user-select: none;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.base li:hover {
|
||||
background: #d5d5d5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.material {
|
||||
.rootSub {
|
||||
list-style: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.material li {
|
||||
color: black;
|
||||
border: none;
|
||||
border-bottom: solid 2px white;
|
||||
background: white;
|
||||
padding: 8px 0;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.material li:hover {
|
||||
background: white;
|
||||
border-bottom: solid 2px grey;
|
||||
margin: 0;
|
||||
border-bottom: solid 2px #eee;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,66 @@
|
||||
import React from 'react';
|
||||
import styles from './TabBar.css';
|
||||
import cn from 'classnames';
|
||||
import Tab from './Tab';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class TabBar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClickTab = this.handleClickTab.bind(this);
|
||||
}
|
||||
|
||||
handleClickTab(tabId) {
|
||||
if (this.props.onChange) {
|
||||
this.props.onChange(tabId);
|
||||
}
|
||||
getRootClassName({className, classNames = {}, sub} = this.props) {
|
||||
return cn(
|
||||
'talk-tab-bar',
|
||||
className,
|
||||
{
|
||||
[classNames.root || styles.root]: !sub,
|
||||
[classNames.rootSub || styles.rootSub]: sub,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, activeTab, cStyle = 'base'} = this.props;
|
||||
const {
|
||||
children,
|
||||
activeTab,
|
||||
tabClassNames,
|
||||
classNames: _a,
|
||||
onTabClick: _b,
|
||||
'aria-controls': ariaControls,
|
||||
sub,
|
||||
...rest,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul className={`${styles.base} ${cStyle ? styles[cStyle] : ''} talk-tab-bar ${this.props.className}`}>
|
||||
{React.Children.toArray(children)
|
||||
.filter((child) => !child.props.restricted)
|
||||
.map((child, tabId) =>
|
||||
React.cloneElement(child, {
|
||||
tabId,
|
||||
active: child.props.id === activeTab,
|
||||
onTabClick: this.handleClickTab,
|
||||
cStyle
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<ul
|
||||
{...rest}
|
||||
role="tablist"
|
||||
className={this.getRootClassName()}
|
||||
>
|
||||
{React.Children.toArray(children)
|
||||
.map((child, i) =>
|
||||
React.cloneElement(child, {
|
||||
tabId: (child.props.tabId !== undefined) ? child.props.tabId : i,
|
||||
active: child.props.tabId === activeTab,
|
||||
onTabClick: this.props.onTabClick,
|
||||
classNames: tabClassNames,
|
||||
'aria-controls': ariaControls,
|
||||
sub,
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
className: PropTypes.string,
|
||||
classNames: PropTypes.shape({
|
||||
root: PropTypes.string,
|
||||
rootSub: PropTypes.string,
|
||||
}),
|
||||
tabClassNames: Tab.propTypes.classNames,
|
||||
activeTab: PropTypes.string,
|
||||
onTabClick: PropTypes.func,
|
||||
sub: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TabBar;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.root {
|
||||
padding-top: 10px;
|
||||
}
|
||||
@@ -1,6 +1,33 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './TabContent.css';
|
||||
|
||||
export default ({children, show = true}) => (
|
||||
show ? <div>{children}</div> : null
|
||||
function getRootClassName(className) {
|
||||
return cn('talk-tab-content', className, styles.root);
|
||||
}
|
||||
|
||||
const TabContent = ({children, className, activeTab, sub, ...rest}) => (
|
||||
<div
|
||||
{...rest}
|
||||
className={getRootClassName(className)}
|
||||
>
|
||||
{
|
||||
React.Children.toArray(children)
|
||||
.filter((child) => child.props.tabId === activeTab)
|
||||
.map((child, i) =>
|
||||
React.cloneElement(child, {
|
||||
tabId: (child.props.tabId !== undefined) ? child.props.tabId : i,
|
||||
sub,
|
||||
}))
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
TabContent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
activeTab: PropTypes.string,
|
||||
sub: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TabContent;
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
|
||||
.root, .rootSub {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
background: #616161;
|
||||
color: white;
|
||||
font-weight: normal;
|
||||
font-size: 10px;
|
||||
padding: 2px;
|
||||
margin-left: 2px;
|
||||
margin-top: -2px;
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.rootSubActive {
|
||||
background: #10589b;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import styles from './TabCount.css';
|
||||
|
||||
function getNumber(no) {
|
||||
let result = Number.parseInt(no);
|
||||
if (no >= 1000) {
|
||||
result = `${Math.round(result / 100) / 10}k`;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRootClassName({className, active, sub}) {
|
||||
return cn(
|
||||
'talk-tab-count',
|
||||
className,
|
||||
{
|
||||
[styles.root]: !sub,
|
||||
[styles.rootSub]: sub,
|
||||
[styles.rootActive]: active && !sub,
|
||||
[styles.rootSubActive]: active && sub,
|
||||
'talk-tab-active': active,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default ({children, active, sub, className}) => (
|
||||
<span className={getRootClassName({className, active, sub})}>{getNumber(children)}</span>
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import cn from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
function getRootClassName(className) {
|
||||
return cn('talk-pane', className);
|
||||
}
|
||||
|
||||
const TabPane = ({children, className, tabId: _a, sub: _b, ...rest}) => (
|
||||
<div
|
||||
{...rest}
|
||||
className={getRootClassName(className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
TabPane.propTypes = {
|
||||
className: PropTypes.string,
|
||||
tabId: PropTypes.string,
|
||||
sub: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default TabPane;
|
||||
@@ -4,7 +4,9 @@ export {default as CoralLogo} from './components/CoralLogo';
|
||||
export {default as FabButton} from './components/FabButton';
|
||||
export {default as TabBar} from './components/TabBar';
|
||||
export {default as Tab} from './components/Tab';
|
||||
export {default as TabCount} from './components/TabCount';
|
||||
export {default as TabContent} from './components/TabContent';
|
||||
export {default as TabPane} from './components/TabPane';
|
||||
export {default as Button} from './components/Button';
|
||||
export {default as Spinner} from './components/Spinner';
|
||||
export {default as Tooltip} from './components/Tooltip';
|
||||
|
||||
@@ -2,6 +2,7 @@ en:
|
||||
your_account_has_been_suspended: Your account has been temporarily suspended.
|
||||
your_account_has_been_banned: Your account has been banned.
|
||||
your_username_has_been_rejected: Your account has been suspended because your username has been deemed inappropriate. To restore your account please enter a new username.
|
||||
embed_comments_tab: Comments
|
||||
bandialog:
|
||||
are_you_sure: "Are you sure you would like to ban {0}?"
|
||||
ban_user: "Ban User?"
|
||||
|
||||
@@ -2,6 +2,7 @@ es:
|
||||
your_account_has_been_suspended: Su cuenta ha sido temporalmente suspendida.
|
||||
your_account_has_been_banned: Su cuenta ha sido suspendida.
|
||||
your_username_has_been_rejected: Su cuenta ha sido suspendida porque tu nombre de usuario ha sido considerado no apropiado para el espacio. Para recuperar la cuenta, por favor ingresar un nuevo nombre de usuario.
|
||||
embed_comments_tab: Comentarios
|
||||
bandialog:
|
||||
are_you_sure: "¿Estás segura que quieres suspender a {0}?"
|
||||
ban_user: "¿Quieres suspender el Usuario?"
|
||||
|
||||
@@ -46,7 +46,7 @@ class LikeButton extends React.Component {
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span>{t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')}</span>
|
||||
<Icon name="thumb_up" className={`${plugin}-icon`} />
|
||||
<Icon name="thumb_up" className={cn(`${plugin}-icon`, styles.icon)} />
|
||||
<span className={`${plugin}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -23,3 +23,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ class LoveButton extends React.Component {
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<span>{t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')}</span>
|
||||
<Icon name="favorite" className={`${plugin}-icon`} />
|
||||
<Icon name="favorite" className={cn(`${plugin}-icon`, styles.icon)} />
|
||||
<span className={`${plugin}-count`}>{count > 0 && count}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -23,3 +23,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
@@ -26,5 +26,5 @@
|
||||
}
|
||||
|
||||
.icon {
|
||||
padding: 0 5px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
.root {
|
||||
float: right;
|
||||
text-align: right;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
min-width: 220px;
|
||||
width: 100%;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,8 +42,8 @@
|
||||
}
|
||||
|
||||
.button {
|
||||
composes: buttonReset from "coral-framework/styles/reset.css";
|
||||
margin: 5px 0px 5px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copyButton {
|
||||
|
||||
Reference in New Issue
Block a user