diff --git a/client/coral-embed-stream/src/actions/stream.js b/client/coral-embed-stream/src/actions/stream.js index 9b5d94a9b..af905a53f 100644 --- a/client/coral-embed-stream/src/actions/stream.js +++ b/client/coral-embed-stream/src/actions/stream.js @@ -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}); +}; diff --git a/client/coral-embed-stream/src/components/AllCommentsPane.js b/client/coral-embed-stream/src/components/AllCommentsPane.js new file mode 100644 index 000000000..6af72475a --- /dev/null +++ b/client/coral-embed-stream/src/components/AllCommentsPane.js @@ -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 ( +
+ + + {view.map((comment) => { + return commentIsIgnored(comment) + ? + : ; + })} + + +
+ ); + } +} + +export default AllCommentsPane; diff --git a/client/coral-embed-stream/src/components/Comment.css b/client/coral-embed-stream/src/components/Comment.css index 4c8a9fc86..d397185dc 100644 --- a/client/coral-embed-stream/src/components/Comment.css +++ b/client/coral-embed-stream/src/components/Comment.css @@ -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; diff --git a/client/coral-embed-stream/src/components/Comment.js b/client/coral-embed-stream/src/components/Comment.js index 0e6201caf..bad2b6e4a 100644 --- a/client/coral-embed-stream/src/components/Comment.js +++ b/client/coral-embed-stream/src/components/Comment.js @@ -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 &&
}
; diff --git a/client/coral-embed-stream/src/components/EditableCommentContent.js b/client/coral-embed-stream/src/components/EditableCommentContent.js index 7e6035d58..06d8e6b9d 100644 --- a/client/coral-embed-stream/src/components/EditableCommentContent.js +++ b/client/coral-embed-stream/src/components/EditableCommentContent.js @@ -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 {
{ - 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 (
- - - {t('framework.my_profile')} - {t('framework.configure_stream')} + + + {t('embed_comments_tab')} + + + {t('framework.my_profile')} + + {can(user, 'UPDATE_CONFIG') && + + {t('framework.configure_stream')} + + } - {commentId && - } - - - - - - - - + + + + + + + + + + +
diff --git a/client/coral-embed-stream/src/components/Stream.css b/client/coral-embed-stream/src/components/Stream.css new file mode 100644 index 000000000..f2c4c48c5 --- /dev/null +++ b/client/coral-embed-stream/src/components/Stream.css @@ -0,0 +1,12 @@ +.root { + margin-top: 6px; +} + +.viewAllButton { + position: absolute; + right: 0px; +} + +.tabPanel { + margin-top: 8px; +} diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 4b04f5e7b..1c84c2d47 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -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 ( -
+
+ {comment && + } {open ?
@@ -211,7 +128,7 @@ class Stream extends React.Component { {t( 'stream.temporarily_suspended', - this.props.root.settings.organizationName, + root.settings.organizationName, timeago(user.suspension.until) )} } @@ -223,10 +140,10 @@ class Stream extends React.Component { />} {showCommentBox && - :
- - - {view.map((comment) => { - return commentIsIgnored(comment) - ? - : ; - })} - - -
} + :
+ + + Featured + + + All Comments {totalCommentCount} + + + + + TODO + + + + + +
+ }
); } diff --git a/client/coral-embed-stream/src/constants/stream.js b/client/coral-embed-stream/src/constants/stream.js index 992767455..622a86c18 100644 --- a/client/coral-embed-stream/src/constants/stream.js +++ b/client/coral-embed-stream/src/constants/stream.js @@ -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'; diff --git a/client/coral-embed-stream/src/containers/Embed.js b/client/coral-embed-stream/src/containers/Embed.js index bf05caf29..fff925a0f 100644 --- a/client/coral-embed-stream/src/containers/Embed.js +++ b/client/coral-embed-stream/src/containers/Embed.js @@ -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, }, diff --git a/client/coral-embed-stream/src/containers/Stream.js b/client/coral-embed-stream/src/containers/Stream.js index 28947b77e..21a29dd38 100644 --- a/client/coral-embed-stream/src/containers/Stream.js +++ b/client/coral-embed-stream/src/containers/Stream.js @@ -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( diff --git a/client/coral-embed-stream/src/reducers/stream.js b/client/coral-embed-stream/src/reducers/stream.js index 2d6e3e8b2..4f4fcf47f 100644 --- a/client/coral-embed-stream/src/reducers/stream.js +++ b/client/coral-embed-stream/src/reducers/stream.js @@ -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, diff --git a/client/coral-embed-stream/style/default.css b/client/coral-embed-stream/style/default.css index bd56cb748..4d7d534dd 100644 --- a/client/coral-embed-stream/style/default.css +++ b/client/coral-embed-stream/style/default.css @@ -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); diff --git a/client/coral-framework/styles/reset.css b/client/coral-framework/styles/reset.css new file mode 100644 index 000000000..14ae4e723 --- /dev/null +++ b/client/coral-framework/styles/reset.css @@ -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; + } +} diff --git a/client/coral-plugin-best/BestButton.css b/client/coral-plugin-best/BestButton.css new file mode 100644 index 000000000..b1f169ff6 --- /dev/null +++ b/client/coral-plugin-best/BestButton.css @@ -0,0 +1,4 @@ +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + margin: 5px 10px 5px 0px; +} diff --git a/client/coral-plugin-best/BestButton.js b/client/coral-plugin-best/BestButton.js index 59204bbb8..ac6d03fd8 100644 --- a/client/coral-plugin-best/BestButton.js +++ b/client/coral-plugin-best/BestButton.js @@ -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 ( diff --git a/client/coral-plugin-comment-count/CommentCount.js b/client/coral-plugin-comment-count/CommentCount.js deleted file mode 100644 index cca99e729..000000000 --- a/client/coral-plugin-comment-count/CommentCount.js +++ /dev/null @@ -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
- {`${count} ${count === 1 ? t('comment_singular') : t('comment_plural')}`} -
; -}; - -CommentCount.propTypes = { - count: PropTypes.number.isRequired -}; - -export default CommentCount; diff --git a/client/coral-plugin-flags/components/styles.css b/client/coral-plugin-flags/components/styles.css index 6de9df387..0990252b4 100644 --- a/client/coral-plugin-flags/components/styles.css +++ b/client/coral-plugin-flags/components/styles.css @@ -1,4 +1,5 @@ .button { + composes: buttonReset from "coral-framework/styles/reset.css"; margin: 5px 0px 5px 10px; } diff --git a/client/coral-plugin-replies/ReplyButton.css b/client/coral-plugin-replies/ReplyButton.css new file mode 100644 index 000000000..b1f169ff6 --- /dev/null +++ b/client/coral-plugin-replies/ReplyButton.css @@ -0,0 +1,4 @@ +.button { + composes: buttonReset from "coral-framework/styles/reset.css"; + margin: 5px 10px 5px 0px; +} diff --git a/client/coral-plugin-replies/ReplyButton.js b/client/coral-plugin-replies/ReplyButton.js index a66e3c156..5c0b6b918 100644 --- a/client/coral-plugin-replies/ReplyButton.js +++ b/client/coral-plugin-replies/ReplyButton.js @@ -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 ( + + ); + } +} + +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; diff --git a/client/coral-ui/components/TabBar.css b/client/coral-ui/components/TabBar.css index b5d44d570..181fa1ddc 100644 --- a/client/coral-ui/components/TabBar.css +++ b/client/coral-ui/components/TabBar.css @@ -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; } diff --git a/client/coral-ui/components/TabBar.js b/client/coral-ui/components/TabBar.js index 08f466ffc..433213a30 100644 --- a/client/coral-ui/components/TabBar.js +++ b/client/coral-ui/components/TabBar.js @@ -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 ( -
-
    - {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 - }) - )} -
-
+
    + {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, + }) + )} +
); } } +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; diff --git a/client/coral-ui/components/TabContent.css b/client/coral-ui/components/TabContent.css new file mode 100644 index 000000000..debafa97c --- /dev/null +++ b/client/coral-ui/components/TabContent.css @@ -0,0 +1,3 @@ +.root { + padding-top: 10px; +} diff --git a/client/coral-ui/components/TabContent.js b/client/coral-ui/components/TabContent.js index 737c9a5e6..768000cb6 100644 --- a/client/coral-ui/components/TabContent.js +++ b/client/coral-ui/components/TabContent.js @@ -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 ?
{children}
: null +function getRootClassName(className) { + return cn('talk-tab-content', className, styles.root); +} + +const TabContent = ({children, className, activeTab, sub, ...rest}) => ( +
+ { + 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, + })) + } +
); +TabContent.propTypes = { + className: PropTypes.string, + activeTab: PropTypes.string, + sub: PropTypes.bool, +}; + +export default TabContent; diff --git a/client/coral-ui/components/TabCount.css b/client/coral-ui/components/TabCount.css new file mode 100644 index 000000000..f95792a0b --- /dev/null +++ b/client/coral-ui/components/TabCount.css @@ -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; +} diff --git a/client/coral-ui/components/TabCount.js b/client/coral-ui/components/TabCount.js new file mode 100644 index 000000000..89ccf1479 --- /dev/null +++ b/client/coral-ui/components/TabCount.js @@ -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}) => ( + {getNumber(children)} +); diff --git a/client/coral-ui/components/TabPane.js b/client/coral-ui/components/TabPane.js new file mode 100644 index 000000000..1bdf19f0a --- /dev/null +++ b/client/coral-ui/components/TabPane.js @@ -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}) => ( +
+ {children} +
+); + +TabPane.propTypes = { + className: PropTypes.string, + tabId: PropTypes.string, + sub: PropTypes.bool, +}; + +export default TabPane; diff --git a/client/coral-ui/index.js b/client/coral-ui/index.js index 3e9456727..8a538d120 100644 --- a/client/coral-ui/index.js +++ b/client/coral-ui/index.js @@ -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'; diff --git a/locales/en.yml b/locales/en.yml index 44db5e1b8..e3bf42e5e 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -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?" diff --git a/locales/es.yml b/locales/es.yml index a6b1bc4cd..fe6030b74 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -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?" diff --git a/plugins/coral-plugin-like/client/LikeButton.js b/plugins/coral-plugin-like/client/LikeButton.js index 7d17ef056..fd5c7d2c8 100644 --- a/plugins/coral-plugin-like/client/LikeButton.js +++ b/plugins/coral-plugin-like/client/LikeButton.js @@ -46,7 +46,7 @@ class LikeButton extends React.Component { onClick={this.handleClick} > {t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')} - + {count > 0 && count}
diff --git a/plugins/coral-plugin-like/client/styles.css b/plugins/coral-plugin-like/client/styles.css index cb372fa47..6e9fdb517 100644 --- a/plugins/coral-plugin-like/client/styles.css +++ b/plugins/coral-plugin-like/client/styles.css @@ -23,3 +23,7 @@ } } } + +.icon { + padding: 0 2px; +} diff --git a/plugins/coral-plugin-love/client/LoveButton.js b/plugins/coral-plugin-love/client/LoveButton.js index 7450ad668..d97803195 100644 --- a/plugins/coral-plugin-love/client/LoveButton.js +++ b/plugins/coral-plugin-love/client/LoveButton.js @@ -46,7 +46,7 @@ class LoveButton extends React.Component { onClick={this.handleClick} > {t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')} - + {count > 0 && count}
diff --git a/plugins/coral-plugin-love/client/styles.css b/plugins/coral-plugin-love/client/styles.css index e16e17ca4..316104e95 100644 --- a/plugins/coral-plugin-love/client/styles.css +++ b/plugins/coral-plugin-love/client/styles.css @@ -23,3 +23,7 @@ } } } + +.icon { + padding: 0 2px; +} diff --git a/plugins/coral-plugin-respect/client/styles.css b/plugins/coral-plugin-respect/client/styles.css index f0b780b86..e984dc3af 100644 --- a/plugins/coral-plugin-respect/client/styles.css +++ b/plugins/coral-plugin-respect/client/styles.css @@ -26,5 +26,5 @@ } .icon { - padding: 0 5px; + padding: 0 2px; } diff --git a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css index 47cda9a1a..a657bbeea 100644 --- a/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css +++ b/plugins/coral-plugin-viewing-options/client/components/ViewingOptions.css @@ -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; } diff --git a/plugins/talk-plugin-permalink/client/components/styles.css b/plugins/talk-plugin-permalink/client/components/styles.css index f14e87881..244cc2acc 100644 --- a/plugins/talk-plugin-permalink/client/components/styles.css +++ b/plugins/talk-plugin-permalink/client/components/styles.css @@ -42,8 +42,8 @@ } .button { + composes: buttonReset from "coral-framework/styles/reset.css"; margin: 5px 0px 5px 10px; - cursor: pointer; } .copyButton {