Implement Stream TabBar

This commit is contained in:
Chi Vinh Le
2017-07-05 22:23:19 +07:00
parent c01d1f95bc
commit 1e4e1cb413
39 changed files with 765 additions and 350 deletions
@@ -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,
+5 -5
View File
@@ -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);
+29
View File
@@ -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;
}
}
+4
View File
@@ -0,0 +1,4 @@
.button {
composes: buttonReset from "coral-framework/styles/reset.css";
margin: 5px 10px 5px 0px;
}
+3 -2
View File
@@ -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;
}
+5 -3
View File
@@ -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>
+61 -6
View File
@@ -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;
}
+85 -9
View File
@@ -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;
+5 -35
View File
@@ -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;
}
+52 -23
View File
@@ -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;
}
+29 -2
View File
@@ -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;
+18
View File
@@ -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;
}
+29
View File
@@ -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>
);
+24
View File
@@ -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;
+2
View File
@@ -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';
+1
View File
@@ -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?"
+1
View File
@@ -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 {