mirror of
https://github.com/wassname/talk.git
synced 2026-08-20 12:50:41 +08:00
First working version
This commit is contained in:
@@ -13,5 +13,6 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
|
||||
node_modules
|
||||
|
||||
@@ -29,5 +29,6 @@ plugins/*
|
||||
!plugins/talk-plugin-comment-content
|
||||
!plugins/talk-plugin-permalink
|
||||
!plugins/talk-plugin-featured-comments
|
||||
!plugins/talk-plugin-sort-oldest
|
||||
|
||||
**/node_modules/*
|
||||
|
||||
@@ -5,6 +5,8 @@ import queryString from 'query-string';
|
||||
|
||||
export const setActiveReplyBox = (id) => ({type: actions.SET_ACTIVE_REPLY_BOX, id});
|
||||
|
||||
export const setSort = ({order, by}) => ({type: actions.SET_SORT, order, by});
|
||||
|
||||
export const viewAllComments = () => {
|
||||
|
||||
const search = queryString.stringify({
|
||||
|
||||
@@ -13,7 +13,7 @@ import t, {timeago} from 'coral-framework/services/i18n';
|
||||
import CommentBox from 'talk-plugin-commentbox/CommentBox';
|
||||
import QuestionBox from 'talk-plugin-questionbox/QuestionBox';
|
||||
import {isCommentActive} from 'coral-framework/utils';
|
||||
import {Button, Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import {Spinner, Button, Tab, TabCount, TabPane} from 'coral-ui';
|
||||
import cn from 'classnames';
|
||||
|
||||
import {getTopLevelParent, attachCommentToParent} from '../graphql/utils';
|
||||
@@ -23,6 +23,8 @@ import StreamTabPanel from '../containers/StreamTabPanel';
|
||||
|
||||
import styles from './Stream.css';
|
||||
|
||||
const SpinnerWhenLoading = ({loading, children}) => loading ? <Spinner /> : <div>{children}</div>;
|
||||
|
||||
class Stream extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
@@ -49,15 +51,14 @@ class Stream extends React.Component {
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
renderHighlightedComment() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
appendItemArray,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comment, comments, totalCommentCount}},
|
||||
root: {asset, asset: {comment}},
|
||||
postComment,
|
||||
addNotification,
|
||||
editComment,
|
||||
@@ -65,23 +66,15 @@ class Stream extends React.Component {
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
updateItem,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
viewAllComments,
|
||||
auth: {loggedIn, user},
|
||||
editName,
|
||||
auth: {user},
|
||||
emit,
|
||||
} = this.props;
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
// even though the permalinked comment is the highlighted one, we're displaying its parent + replies
|
||||
let highlightedComment = comment && getTopLevelParent(comment);
|
||||
if (highlightedComment) {
|
||||
let topLevelComment = getTopLevelParent(comment);
|
||||
if (topLevelComment) {
|
||||
|
||||
// Inactive comments can be viewed by moderators and admins (e.g. using permalinks).
|
||||
const isInactive = !isCommentActive(comment.status);
|
||||
@@ -89,10 +82,152 @@ class Stream extends React.Component {
|
||||
|
||||
// the highlighted comment is not active and as such not in the replies, so we
|
||||
// attach it to the right parent.
|
||||
highlightedComment = attachCommentToParent(highlightedComment, comment);
|
||||
topLevelComment = attachCommentToParent(topLevelComment, comment);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
|
||||
<Comment
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={topLevelComment.id}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
comment={topLevelComment}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderTabPanel() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
activeReplyBox,
|
||||
setActiveReplyBox,
|
||||
commentClassNames,
|
||||
root: {asset, asset: {comments, totalCommentCount}},
|
||||
postComment,
|
||||
addNotification,
|
||||
editComment,
|
||||
postFlag,
|
||||
postDontAgree,
|
||||
deleteAction,
|
||||
showSignInDialog,
|
||||
ignoreUser,
|
||||
activeStreamTab,
|
||||
setActiveStreamTab,
|
||||
loadNewReplies,
|
||||
loadMoreComments,
|
||||
auth: {user},
|
||||
emit,
|
||||
sortOrder,
|
||||
sortBy,
|
||||
} = this.props;
|
||||
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
// `key` of `StreamTabPanel` depends on sorting so that we always reset
|
||||
// the state when changing sorting.
|
||||
return (
|
||||
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
|
||||
<div
|
||||
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
|
||||
>
|
||||
<Slot
|
||||
fill="streamFilter"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</div>
|
||||
<StreamTabPanel
|
||||
key={`${sortBy}_${sortOrder}`}
|
||||
activeTab={activeStreamTab}
|
||||
setActiveTab={setActiveStreamTab}
|
||||
fallbackTab={'all'}
|
||||
tabSlot={'streamTabs'}
|
||||
tabPaneSlot={'streamTabPanes'}
|
||||
slotProps={slotProps}
|
||||
queryData={slotQueryData}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
<TabPane tabId={'all'} key='all'>
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
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={this.commentIsIgnored}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
sub
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
root,
|
||||
appendItemArray,
|
||||
root: {asset, asset: {comment: highlightedComment, comments}},
|
||||
postComment,
|
||||
addNotification,
|
||||
updateItem,
|
||||
viewAllComments,
|
||||
auth: {loggedIn, user},
|
||||
editName,
|
||||
loading,
|
||||
} = this.props;
|
||||
const {keepCommentBox} = this.state;
|
||||
const open = !asset.isClosed;
|
||||
|
||||
const banned = user && user.status === 'BANNED';
|
||||
const temporarilySuspended =
|
||||
user &&
|
||||
@@ -103,7 +238,7 @@ class Stream extends React.Component {
|
||||
const slotProps = {data};
|
||||
const slotQueryData = {root, asset};
|
||||
|
||||
if (!comment && !comments) {
|
||||
if (!highlightedComment && !comments) {
|
||||
console.error('Talk: No comments came back from the graph given that query. Please, check the query params.');
|
||||
return <StreamError />;
|
||||
}
|
||||
@@ -111,7 +246,7 @@ class Stream extends React.Component {
|
||||
return (
|
||||
<div id="stream" className={styles.root}>
|
||||
<AutomaticAssetClosure assetId={asset.id} closedAt={asset.closedAt}/>
|
||||
{comment &&
|
||||
{highlightedComment &&
|
||||
<Button
|
||||
cStyle="darkGrey"
|
||||
className={cn('talk-stream-show-all-comments-button', styles.viewAllButton)}
|
||||
@@ -174,96 +309,12 @@ class Stream extends React.Component {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* the highlightedComment is isolated after the user followed a permalink */}
|
||||
{highlightedComment
|
||||
? (
|
||||
<div className={cn('talk-stream-highlighted-container', styles.highlightedContainer)}>
|
||||
<Comment
|
||||
data={data}
|
||||
root={root}
|
||||
commentClassNames={commentClassNames}
|
||||
ignoreUser={ignoreUser}
|
||||
setActiveReplyBox={setActiveReplyBox}
|
||||
activeReplyBox={activeReplyBox}
|
||||
addNotification={addNotification}
|
||||
depth={0}
|
||||
disableReply={!open}
|
||||
postComment={postComment}
|
||||
asset={asset}
|
||||
currentUser={user}
|
||||
highlighted={comment.id}
|
||||
postFlag={postFlag}
|
||||
postDontAgree={postDontAgree}
|
||||
loadMore={loadNewReplies}
|
||||
deleteAction={deleteAction}
|
||||
showSignInDialog={showSignInDialog}
|
||||
key={highlightedComment.id}
|
||||
commentIsIgnored={this.commentIsIgnored}
|
||||
comment={highlightedComment}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
liveUpdates={true}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: <div className={cn('talk-stream-tab-container', styles.tabContainer)}>
|
||||
<div
|
||||
className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}
|
||||
>
|
||||
<Slot
|
||||
fill="streamFilter"
|
||||
queryData={slotQueryData}
|
||||
{...slotProps}
|
||||
/>
|
||||
</div>
|
||||
<StreamTabPanel
|
||||
activeTab={activeStreamTab}
|
||||
setActiveTab={setActiveStreamTab}
|
||||
fallbackTab={'all'}
|
||||
tabSlot={'streamTabs'}
|
||||
tabPaneSlot={'streamTabPanes'}
|
||||
slotProps={slotProps}
|
||||
queryData={slotQueryData}
|
||||
appendTabs={
|
||||
<Tab tabId={'all'} key='all'>
|
||||
All Comments <TabCount active={activeStreamTab === 'all'} sub>{totalCommentCount}</TabCount>
|
||||
</Tab>
|
||||
}
|
||||
appendTabPanes={
|
||||
<TabPane tabId={'all'} key='all'>
|
||||
<AllCommentsPane
|
||||
data={data}
|
||||
root={root}
|
||||
comments={comments}
|
||||
commentClassNames={commentClassNames}
|
||||
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={this.commentIsIgnored}
|
||||
charCountEnable={asset.settings.charCountEnable}
|
||||
maxCharCount={asset.settings.charCount}
|
||||
editComment={editComment}
|
||||
emit={emit}
|
||||
/>
|
||||
</TabPane>
|
||||
}
|
||||
sub
|
||||
/>
|
||||
</div>
|
||||
<SpinnerWhenLoading loading={loading}>
|
||||
{highlightedComment
|
||||
? this.renderHighlightedComment()
|
||||
: this.renderTabPanel()
|
||||
}
|
||||
</SpinnerWhenLoading>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ 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';
|
||||
export const SET_SORT = 'CORAL_STREAM_SET_SORT';
|
||||
|
||||
@@ -158,7 +158,7 @@ const EMBED_QUERY = gql`
|
||||
$hasComment: Boolean!,
|
||||
$excludeIgnored: Boolean,
|
||||
$sortBy: SORT_COMMENTS_BY!,
|
||||
$sort: SORT_ORDER!,
|
||||
$sortOrder: SORT_ORDER!,
|
||||
) {
|
||||
me {
|
||||
id
|
||||
@@ -171,7 +171,7 @@ const EMBED_QUERY = gql`
|
||||
`;
|
||||
|
||||
export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
options: ({auth, commentId, assetId, assetUrl, sortBy, sort}) => ({
|
||||
options: ({auth, commentId, assetId, assetUrl, sortBy, sortOrder}) => ({
|
||||
variables: {
|
||||
assetId,
|
||||
assetUrl,
|
||||
@@ -179,7 +179,7 @@ export const withEmbedQuery = withQuery(EMBED_QUERY, {
|
||||
hasComment: commentId !== '',
|
||||
excludeIgnored: Boolean(auth && auth.user && auth.user.id),
|
||||
sortBy,
|
||||
sort,
|
||||
sortOrder,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -191,7 +191,7 @@ const mapStateToProps = (state) => ({
|
||||
assetUrl: state.stream.assetUrl,
|
||||
activeTab: state.embed.activeTab,
|
||||
config: state.config,
|
||||
sort: state.stream.sort,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
});
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ class StreamContainer extends React.Component {
|
||||
cursor: comment.replies.endCursor,
|
||||
parent_id,
|
||||
asset_id: this.props.root.asset.id,
|
||||
sort: 'ASC',
|
||||
sortOrder: 'ASC',
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (prev, {fetchMoreResult:{comments}}) => {
|
||||
@@ -119,7 +119,7 @@ class StreamContainer extends React.Component {
|
||||
cursor: this.props.root.asset.comments.endCursor,
|
||||
parent_id: null,
|
||||
asset_id: this.props.root.asset.id,
|
||||
sort: this.props.data.variables.sort,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
@@ -141,24 +141,43 @@ class StreamContainer extends React.Component {
|
||||
clearInterval(this.countPoll);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.sortBy === 'CREATED_AT' && nextProps.sortOrder === 'DESC') {
|
||||
|
||||
// When switching to 'Newest first' we refetch and subscribe so that we always have the newest comments.
|
||||
if (this.props.sortOrder !== nextProps.sortOrder || this.props.sortBy !== nextProps.sortBy) {
|
||||
nextProps.data.refetch();
|
||||
this.subscribeToUpdates();
|
||||
}
|
||||
} else {
|
||||
|
||||
// TODO: only unsubscribe from posting comments.
|
||||
// We only subscribe to new comments during 'Newest first'.
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
userIsDegraged({auth: {user}} = this.props) {
|
||||
return !can(user, 'INTERACT_WITH_COMMUNITY');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.refetching
|
||||
|| !this.props.root.asset
|
||||
if (!this.props.root.asset
|
||||
|| !this.props.root.asset.comment
|
||||
&& !this.props.root.asset.comments
|
||||
) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
const streamLoading = this.props.refetching || this.props.data.loading;
|
||||
|
||||
return <Stream
|
||||
{...this.props}
|
||||
loadMore={this.loadMore}
|
||||
loadMoreComments={this.loadMoreComments}
|
||||
loadNewReplies={this.loadNewReplies}
|
||||
userIsDegraged={this.userIsDegraged()}
|
||||
loading={streamLoading}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
@@ -209,7 +228,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
$cursor: Cursor
|
||||
$parent_id: ID
|
||||
$asset_id: ID
|
||||
$sort: SORT_ORDER
|
||||
$sortOrder: SORT_ORDER
|
||||
$sortBy: SORT_COMMENTS_BY = CREATED_AT
|
||||
$excludeIgnored: Boolean
|
||||
) {
|
||||
@@ -219,7 +238,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
cursor: $cursor
|
||||
parent_id: $parent_id
|
||||
asset_id: $asset_id
|
||||
sort: $sort
|
||||
sort: $sortOrder
|
||||
sortBy: $sortBy
|
||||
excludeIgnored: $excludeIgnored
|
||||
}
|
||||
@@ -274,7 +293,7 @@ const fragments = {
|
||||
}
|
||||
commentCount @skip(if: $hasComment)
|
||||
totalCommentCount @skip(if: $hasComment)
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sort, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
comments(query: {limit: 10, excludeIgnored: $excludeIgnored, sort: $sortOrder, sortBy: $sortBy}) @skip(if: $hasComment) {
|
||||
nodes {
|
||||
...CoralEmbedStream_Stream_comment
|
||||
}
|
||||
@@ -317,6 +336,8 @@ const mapStateToProps = (state) => ({
|
||||
previousStreamTab: state.stream.previousTab,
|
||||
commentClassNames: state.stream.commentClassNames,
|
||||
pluginConfig: state.config.plugin_config,
|
||||
sortOrder: state.stream.sortOrder,
|
||||
sortBy: state.stream.sortBy,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
|
||||
@@ -24,7 +24,7 @@ const initialState = {
|
||||
activeTab: process.env.TALK_DEFAULT_STREAM_TAB,
|
||||
previousTab: '',
|
||||
sortBy: 'CREATED_AT',
|
||||
sort: 'DESC',
|
||||
sortOrder: 'DESC',
|
||||
};
|
||||
|
||||
export default function stream(state = initialState, action) {
|
||||
@@ -68,6 +68,12 @@ export default function stream(state = initialState, action) {
|
||||
...state.commentClassNames.slice(action.idx + 1)
|
||||
]
|
||||
};
|
||||
case actions.SET_SORT :
|
||||
return {
|
||||
...state,
|
||||
sortOrder: action.order ? action.order : state.order,
|
||||
sortBy: action.by ? action.by : state.by,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {getSlotElements, getSlotComponentProps} from 'coral-framework/helpers/pl
|
||||
import omit from 'lodash/omit';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import {getShallowChanges} from 'coral-framework/utils';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const emptyConfig = {};
|
||||
|
||||
@@ -25,7 +26,18 @@ class Slot extends React.Component {
|
||||
return changes.length !== 0;
|
||||
}
|
||||
|
||||
getSlotProps({fill: _a, inline: _b, className: _c, reduxState: _d, defaultComponent_: _e, queryData: _f, ...rest} = this.props) {
|
||||
getSlotProps(props = this.props) {
|
||||
const {
|
||||
fill: _a,
|
||||
inline: _b,
|
||||
className: _c,
|
||||
reduxState: _d,
|
||||
defaultComponent_: _e,
|
||||
queryData: _f,
|
||||
childFactory: _g,
|
||||
component: _h,
|
||||
...rest
|
||||
} = props;
|
||||
return rest;
|
||||
}
|
||||
|
||||
@@ -34,26 +46,60 @@ class Slot extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {inline = false, className, reduxState, defaultComponent: DefaultComponent, queryData} = this.props;
|
||||
const {
|
||||
inline = false,
|
||||
className,
|
||||
reduxState,
|
||||
component: Component,
|
||||
childFactory,
|
||||
defaultComponent: DefaultComponent,
|
||||
queryData,
|
||||
} = this.props;
|
||||
let children = this.getChildren();
|
||||
const pluginConfig = reduxState.config.pluginConfig || emptyConfig;
|
||||
if (children.length === 0 && DefaultComponent) {
|
||||
children = <DefaultComponent {...getSlotComponentProps(DefaultComponent, reduxState, this.getSlotProps(this.props), queryData)} />;
|
||||
}
|
||||
|
||||
if (childFactory) {
|
||||
children = children.map(childFactory);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
|
||||
<Component className={cn({[styles.inline]: inline, [styles.debug]: pluginConfig.debug}, className)}>
|
||||
{children}
|
||||
</div>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Slot.defaultProps = {
|
||||
component: 'div',
|
||||
};
|
||||
|
||||
Slot.propTypes = {
|
||||
fill: React.PropTypes.string.isRequired,
|
||||
fill: PropTypes.string.isRequired,
|
||||
|
||||
/**
|
||||
* You may specify the component to use as the root wrapper.
|
||||
* Defaults to 'div'.
|
||||
*/
|
||||
component: PropTypes.any,
|
||||
|
||||
// props coming from graphql must be passed through this property.
|
||||
queryData: React.PropTypes.object,
|
||||
queryData: PropTypes.object,
|
||||
|
||||
/**
|
||||
* You may need to apply reactive updates to a child as it is exiting.
|
||||
* This is generally done by using `cloneElement` however in the case of an exiting
|
||||
* child the element has already been removed and not accessible to the consumer.
|
||||
*
|
||||
* If you do need to update a child as it leaves you can provide a `childFactory`
|
||||
* to wrap every child, even the ones that are leaving.
|
||||
*
|
||||
* @type Function(child: ReactElement) -> ReactElement
|
||||
*/
|
||||
childFactory: PropTypes.func,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
|
||||
@@ -2,6 +2,7 @@ import {gql} from 'react-apollo';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import union from 'lodash/union';
|
||||
import {capitalize} from 'coral-framework/helpers/strings';
|
||||
export * from 'coral-framework/helpers/strings';
|
||||
|
||||
export const getTotalActionCount = (type, comment) => {
|
||||
return comment.action_summaries
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export {setSort} from 'coral-embed-stream/src/actions/stream';
|
||||
@@ -0,0 +1,8 @@
|
||||
.input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
cursor: pointer;
|
||||
padding-left: 4px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import styles from './SortOption.css';
|
||||
|
||||
export default class SortOption extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.viewingOption}>
|
||||
<input type="radio" onChange={this.props.setSort} checked={this.props.active} className={styles.input}/>
|
||||
<label className={styles.label} onClick={this.props.setSort}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
export {Slot} from 'coral-framework/components';
|
||||
export {default as ClickOutside} from 'coral-framework/components/ClickOutside';
|
||||
export {default as IfSlotIsEmpty} from 'coral-framework/components/IfSlotIsEmpty';
|
||||
export {default as IfSlotIsNotEmpty} from 'coral-framework/components/IfSlotIsNotEmpty';
|
||||
export {default as CommentAuthorName} from 'coral-framework/components/CommentAuthorName';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export {default as withReaction} from './withReaction';
|
||||
export {default as withTags} from './withTags';
|
||||
export {default as withSortOption} from './withSortOption';
|
||||
export {default as withFragments} from 'coral-framework/hocs/withFragments';
|
||||
export {default as excludeIf} from 'coral-framework/hocs/excludeIf';
|
||||
export {default as connect} from 'coral-framework/hocs/connect';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'plugin-api/beta/client/hocs';
|
||||
import {bindActionCreators} from 'redux';
|
||||
import {sortOrderSelector, sortBySelector} from 'plugin-api/beta/client/selectors/stream';
|
||||
import {setSort} from 'plugin-api/beta/client/actions/stream';
|
||||
import hoistStatics from 'recompose/hoistStatics';
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
sortOrder: sortOrderSelector(state),
|
||||
sortBy: sortBySelector(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) =>
|
||||
bindActionCreators(
|
||||
{
|
||||
setSort
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
export default ({by = 'created_at', order = 'DESC', label}) => hoistStatics((WrappedComponent) => {
|
||||
class WithSortOption extends React.Component {
|
||||
setSort = () => {
|
||||
this.props.setSort({by, order});
|
||||
}
|
||||
|
||||
render() {
|
||||
const active = this.props.sortOrder === order && this.props.sortBy === by;
|
||||
const resolvedLabel = typeof label === 'function' ? label() : label;
|
||||
return (
|
||||
<WrappedComponent
|
||||
active={active}
|
||||
setSort={this.setSort}
|
||||
label={resolvedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
return connect(mapStateToProps, mapDispatchToProps)(WithSortOption);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export const sortOrderSelector = (state) => state.stream.sortOrder;
|
||||
export const sortBySelector = (state) => state.stream.sortBy;
|
||||
@@ -3,5 +3,6 @@ export {
|
||||
insertCommentsSorted,
|
||||
getSlotFragmentSpreads,
|
||||
forEachError,
|
||||
capitalize,
|
||||
getDefinitionName,
|
||||
} from 'coral-framework/utils';
|
||||
|
||||
@@ -18,7 +18,7 @@ class TabPaneContainer extends React.Component {
|
||||
limit: 5,
|
||||
cursor: this.props.asset.featuredComments.endCursor,
|
||||
asset_id: this.props.asset.id,
|
||||
sort: this.props.data.variables.sort,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
},
|
||||
@@ -52,7 +52,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
$limit: Int = 5
|
||||
$cursor: Cursor
|
||||
$asset_id: ID
|
||||
$sort: SORT_ORDER
|
||||
$sortOrder: SORT_ORDER
|
||||
$sortBy: SORT_COMMENTS_BY
|
||||
$excludeIgnored: Boolean
|
||||
) {
|
||||
@@ -62,7 +62,7 @@ const LOAD_MORE_QUERY = gql`
|
||||
cursor: $cursor
|
||||
tags: ["FEATURED"]
|
||||
asset_id: $asset_id,
|
||||
sort: $sort
|
||||
sort: $sortOrder
|
||||
sortBy: $sortBy
|
||||
excludeIgnored: $excludeIgnored
|
||||
}
|
||||
@@ -100,7 +100,7 @@ const enhance = compose(
|
||||
featuredComments: comments(
|
||||
query: {
|
||||
tags: ["FEATURED"]
|
||||
sort: $sort
|
||||
sort: $sortOrder
|
||||
sortBy: $sortBy
|
||||
}
|
||||
deep: true
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
}
|
||||
|
||||
.viewingOption {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
:global(.talk-plugin-off-topic-comment) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import reducer from './reducer';
|
||||
|
||||
/**
|
||||
* talk-plugin-offtopic depends on talk-plugin-viewing-options
|
||||
* in other to display filter and use the streamViewingOptions slot
|
||||
* in other to display filter.
|
||||
*/
|
||||
|
||||
export default {
|
||||
@@ -15,6 +15,6 @@ export default {
|
||||
slots: {
|
||||
commentInputDetailArea: [OffTopicCheckbox],
|
||||
commentInfoBar: [OffTopicTag],
|
||||
viewingOptions: [OffTopicFilter]
|
||||
viewingOptionsFilter: [OffTopicFilter]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"presets": [
|
||||
"es2015"
|
||||
],
|
||||
"plugins": [
|
||||
"add-module-exports",
|
||||
"transform-class-properties",
|
||||
"transform-decorators-legacy",
|
||||
"transform-object-assign",
|
||||
"transform-object-rest-spread",
|
||||
"transform-async-to-generator",
|
||||
"transform-react-jsx"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"mocha": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"experimentalObjectRestSpread": true,
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"plugins": [
|
||||
"react"
|
||||
],
|
||||
"rules": {
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
"no-console": ["warn", { "allow": ["warn", "error"] }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
.input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.label {
|
||||
cursor: pointer;
|
||||
padding-left: 4px;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import styles from './SortOption.css';
|
||||
|
||||
export default class SortOption extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.viewingOption}>
|
||||
<input type="radio" onChange={this.props.setSort} checked={this.props.active} className={styles.input}/>
|
||||
<label className={styles.label} onClick={this.props.setSort}>
|
||||
{this.props.label}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import {withSortOption} from 'plugin-api/beta/client/hocs';
|
||||
import SortOption from '../components/SortOption';
|
||||
|
||||
export default withSortOption({by: 'CREATED_AT', order: 'ASC', label: 'Oldest first'})(SortOption);
|
||||
@@ -0,0 +1,4 @@
|
||||
import {withSortOption} from 'plugin-api/beta/client/hocs';
|
||||
import SortOption from '../components/SortOption';
|
||||
|
||||
export default withSortOption({by: 'CREATED_AT', order: 'DESC', label: 'Newest first'})(SortOption);
|
||||
@@ -0,0 +1,15 @@
|
||||
import translations from './translations.yml';
|
||||
import SortOption from './containers/SortOption';
|
||||
import SortOptionNewest from './containers/SortOptionNewest';
|
||||
|
||||
/**
|
||||
* talk-plugin-sort-oldest depends on talk-plugin-viewing-options
|
||||
* in other to display sort option.
|
||||
*/
|
||||
|
||||
export default {
|
||||
translations,
|
||||
slots: {
|
||||
viewingOptionsSort: [SortOptionNewest, SortOption]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
en:
|
||||
talk-plugin-sort-oldest:
|
||||
es:
|
||||
talk-plugin-sort-oldest:
|
||||
@@ -0,0 +1,2 @@
|
||||
module.exports = {
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
.title {
|
||||
font-weight: bold;
|
||||
padding: 12px 8px 0px 8px;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
padding: 10px;
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import styles from './Category.css';
|
||||
import {Slot, IfSlotIsNotEmpty} from 'plugin-api/beta/client/components';
|
||||
|
||||
const childFactory = (child) => <li className={styles.listItem} key={child.key}>{child}</li>;
|
||||
|
||||
const ViewingOptions = ({slot, title}) => {
|
||||
return (
|
||||
<IfSlotIsNotEmpty slot={slot}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
<Slot fill={slot} childFactory={childFactory} className={styles.list} component={'ul'}/>
|
||||
</IfSlotIsNotEmpty>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewingOptions;
|
||||
@@ -9,13 +9,15 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.list {
|
||||
.menu {
|
||||
background: white;
|
||||
position: absolute;
|
||||
box-shadow: 0px 3px 5px 0px rgba(0,0,0,0.15);
|
||||
right: 0px;
|
||||
top: 20px;
|
||||
z-index: 10;
|
||||
min-height: 32px;
|
||||
min-width: 64px;
|
||||
}
|
||||
|
||||
.list > ul, .list > ul > li {
|
||||
|
||||
@@ -3,51 +3,60 @@ import cn from 'classnames';
|
||||
import styles from './ViewingOptions.css';
|
||||
import {t} from 'plugin-api/beta/client/services';
|
||||
import {Icon} from 'plugin-api/beta/client/components/ui';
|
||||
import {Slot, ClickOutside} from 'plugin-api/beta/client/components';
|
||||
import {ClickOutside} from 'plugin-api/beta/client/components';
|
||||
import {capitalize} from 'plugin-api/beta/client/utils';
|
||||
import Category from './Category';
|
||||
|
||||
const ViewingOptions = (props) => {
|
||||
const toggleOpen = () => {
|
||||
if (!props.open) {
|
||||
props.openMenu();
|
||||
class ViewingOptions extends React.Component {
|
||||
|
||||
categories = {
|
||||
sort: t('talk-plugin-viewing-options.sort'),
|
||||
filter: t('talk-plugin-viewing-options.filter'),
|
||||
};
|
||||
|
||||
toggleOpen = () => {
|
||||
const {open, openMenu, closeMenu} = this.props;
|
||||
if (!open) {
|
||||
openMenu();
|
||||
} else {
|
||||
props.closeMenu();
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
if (props.open) {
|
||||
props.closeMenu();
|
||||
handleClickOutside = () => {
|
||||
const {open, closeMenu} = this.props;
|
||||
if (open) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={handleClickOutside}>
|
||||
<div className={cn([styles.root, 'talk-plugin-viewing-options'])}>
|
||||
<div>
|
||||
<button className={styles.button} onClick={toggleOpen}>
|
||||
<Icon className={styles.filterIcon} name="filter_list" />
|
||||
<span className={styles.filterText}>{t('talk-plugin-viewing-options.viewing_options')}</span>
|
||||
{props.open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
props.open ? (
|
||||
<div className={cn([styles.list, 'talk-plugin-viewing-options-list'])}>
|
||||
<ul>
|
||||
render() {
|
||||
const {open} = this.props;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={cn([styles.root, 'talk-plugin-viewing-options'])}>
|
||||
<div>
|
||||
<button className={styles.button} onClick={this.toggleOpen}>
|
||||
<Icon className={styles.filterIcon} name="filter_list" />
|
||||
<span className={styles.filterText}>{t('talk-plugin-viewing-options.viewing_options')}</span>
|
||||
{open ? <Icon name="arrow_drop_up" className={styles.icon}/> : <Icon name="arrow_drop_down" className={styles.icon}/>}
|
||||
</button>
|
||||
</div>
|
||||
{
|
||||
open ? (
|
||||
<div className={cn([styles.menu, 'talk-plugin-viewing-options-menu'])}>
|
||||
{
|
||||
React.Children.map(<Slot fill="viewingOptions" />, (component) => {
|
||||
return React.createElement('li', {
|
||||
className: 'talk-plugin-viewing-options-item'
|
||||
}, component);
|
||||
})
|
||||
Object.keys(this.categories).map((category) =>
|
||||
<Category key={category} slot={`viewingOptions${capitalize(category)}`} title={this.categories[category]} />
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ViewingOptions;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
en:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "Viewing Options"
|
||||
sort: Sort
|
||||
filter: Filter
|
||||
es:
|
||||
talk-plugin-viewing-options:
|
||||
viewing_options: "Opciones de visualización"
|
||||
|
||||
Reference in New Issue
Block a user