diff --git a/.eslintrc.json b/.eslintrc.json index 2186efd8b..12355ac34 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -53,7 +53,8 @@ "no-lonely-if": [2], "curly": [2], "no-unused-vars": ["error", { - "argsIgnorePattern": "next" + "argsIgnorePattern": "^_|next", + "varsIgnorePattern": "^_" }], "no-multiple-empty-lines": ["error", { "max": 1 diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js index 3bde5f931..385076b21 100644 --- a/client/coral-admin/src/AppRouter.js +++ b/client/coral-admin/src/AppRouter.js @@ -50,7 +50,7 @@ const routes = ( - + diff --git a/client/coral-admin/src/routes/Moderation/components/Moderation.js b/client/coral-admin/src/routes/Moderation/components/Moderation.js index 2de15f2ff..eb0b21c46 100644 --- a/client/coral-admin/src/routes/Moderation/components/Moderation.js +++ b/client/coral-admin/src/routes/Moderation/components/Moderation.js @@ -206,6 +206,7 @@ export default class Moderation extends Component { )} { - this.props.loadMore(this.props.activeTab); + if (!this.isLoadingMore) { + this.isLoadingMore = true; + this.props.loadMore(this.props.activeTab) + .then(() => this.isLoadingMore = false) + .catch((e) => { + this.isLoadingMore = false; + throw e; + }); + } } constructor(props) { diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.css b/client/coral-admin/src/routes/Moderation/components/StorySearch.css index f0503f6b9..2df130ec4 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.css +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.css @@ -49,12 +49,14 @@ font-weight: 300; } -.cta { - letter-spacing: 1px; +.cta > a { + display: inline-block; + cursor: pointer; + color: #000; + text-decoration: none; font-weight: bold; font-size: 15px; margin: 0; - height: 50px; box-sizing: border-box; font-size: 15px; font-weight: 500; diff --git a/client/coral-admin/src/routes/Moderation/components/StorySearch.js b/client/coral-admin/src/routes/Moderation/components/StorySearch.js index f42ceaadd..5fe82fa97 100644 --- a/client/coral-admin/src/routes/Moderation/components/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/components/StorySearch.js @@ -38,7 +38,11 @@ const StorySearch = (props) => {
-

Moderate comments on All Stories

+ {props.assetId && +
+ Moderate comments on All Stories +
+ }
{ @@ -87,7 +91,8 @@ StorySearch.propTypes = { goToStory: PropTypes.func.isRequired, closeSearch: PropTypes.func.isRequired, moderation: PropTypes.object.isRequired, - handleSearchChange: PropTypes.func.isRequired + handleSearchChange: PropTypes.func.isRequired, + assetId: PropTypes.string }; export default StorySearch; diff --git a/client/coral-admin/src/routes/Moderation/components/UserDetail.js b/client/coral-admin/src/routes/Moderation/components/UserDetail.js index 7047a41d5..59f696b5f 100644 --- a/client/coral-admin/src/routes/Moderation/components/UserDetail.js +++ b/client/coral-admin/src/routes/Moderation/components/UserDetail.js @@ -5,6 +5,7 @@ import {Button, Drawer} from 'coral-ui'; import {Slot} from 'coral-framework/components'; import ButtonCopyToClipboard from './ButtonCopyToClipboard'; import {actionsMap} from '../helpers/moderationQueueActionsMap'; +import ClickOutside from 'coral-framework/components/ClickOutside'; export default class UserDetail extends React.Component { @@ -80,90 +81,92 @@ export default class UserDetail extends React.Component { } return ( - -

{user.username}

+ + +

{user.username}

-
- {profile && this.profile = ref} value={profile} />} - -
+
+ {profile && this.profile = ref} value={profile} />} + +
- -

Member since {new Date(user.created_at).toLocaleString()}

-
-

- Account summary -
Data represents the last six months of activity -

-
-
-

Total Comments

-

{totalComments}

-
-
-

Reject Rate

-

{`${(rejectedPercent).toFixed(1)}%`}

-
-
- { - selectedIds.length === 0 - ? ( -
    -
  • All
  • -
  • Rejected
  • -
- ) - : ( -
- - - {`${selectedIds.length} comments selected`} + +

Member since {new Date(user.created_at).toLocaleString()}

+
+

+ Account summary +
Data represents the last six months of activity +

+
+
+

Total Comments

+

{totalComments}

- ) - } - -
+
+

Reject Rate

+

{`${(rejectedPercent).toFixed(1)}%`}

+
+
{ - nodes.map((comment, i) => { - const status = comment.action_summaries ? 'FLAGGED' : comment.status; - const selected = selectedIds.indexOf(comment.id) !== -1; - return {}} - actions={actionsMap[status]} - showBanUserDialog={showBanUserDialog} - showSuspendUserDialog={showSuspendUserDialog} - acceptComment={this.acceptThenReload} - rejectComment={this.rejectThenReload} - selected={selected} - toggleSelect={toggleSelect} - currentAsset={null} - currentUserId={this.props.id} - minimal={true} />; - }) + selectedIds.length === 0 + ? ( +
    +
  • All
  • +
  • Rejected
  • +
+ ) + : ( +
+ + + {`${selectedIds.length} comments selected`} +
+ ) } -
- + +
+ { + nodes.map((comment, i) => { + const status = comment.action_summaries ? 'FLAGGED' : comment.status; + const selected = selectedIds.indexOf(comment.id) !== -1; + return {}} + actions={actionsMap[status]} + showBanUserDialog={showBanUserDialog} + showSuspendUserDialog={showSuspendUserDialog} + acceptComment={this.acceptThenReload} + rejectComment={this.rejectThenReload} + selected={selected} + toggleSelect={toggleSelect} + currentAsset={null} + currentUserId={this.props.id} + minimal={true} />; + }) + } +
+ + ); } } diff --git a/client/coral-admin/src/routes/Moderation/components/styles.css b/client/coral-admin/src/routes/Moderation/components/styles.css index 8b4fe971d..1228c61a4 100644 --- a/client/coral-admin/src/routes/Moderation/components/styles.css +++ b/client/coral-admin/src/routes/Moderation/components/styles.css @@ -190,8 +190,6 @@ span { } &.selected { - max-width: 720px; - max-height: 410px; } .context { diff --git a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js index cf47ca662..f4ab6f23e 100644 --- a/client/coral-admin/src/routes/Moderation/containers/StorySearch.js +++ b/client/coral-admin/src/routes/Moderation/containers/StorySearch.js @@ -41,7 +41,13 @@ class StorySearchContainer extends React.Component { goToStory = (id) => { const {router, closeSearch} = this.props; - router.push(`/admin/moderate/${id}`); + router.push(`/admin/moderate/all/${id}`); + closeSearch(); + } + + goToModerateAll = () => { + const {router, closeSearch} = this.props; + router.push('/admin/moderate/all'); closeSearch(); } @@ -50,6 +56,7 @@ class StorySearchContainer extends React.Component { { }; export default class Comment extends React.Component { + isLoadingReplies = false; + constructor(props) { super(props); @@ -211,14 +213,24 @@ export default class Comment extends React.Component { } loadNewReplies = () => { - const {replies, replyCount, id} = this.props.comment; - if (replyCount > replies.nodes.length) { - this.props.loadMore(id).then(() => { - this.setState(resetCursors(this.state, this.props)); - }); - return; + if (!this.isLoadingReplies) { + this.isLoadingReplies = true; + const {replies, replyCount, id} = this.props.comment; + if (replyCount > replies.nodes.length) { + this.props.loadMore(id) + .then(() => { + this.setState(resetCursors(this.state, this.props)); + this.isLoadingReplies = false; + }) + .catch((e) => { + this.isLoadingReplies = false; + throw e; + }); + return; + } + this.setState(resetCursors); + this.isLoadingReplies = false; } - this.setState(resetCursors); }; showReplyBox = () => { @@ -432,28 +444,28 @@ export default class Comment extends React.Component { inline /> - { (currentUser && - (comment.user.id === currentUser.id)) + { (currentUser && (comment.user.id === currentUser.id)) && - /* User can edit/delete their own comment for a short window after posting */ - ? - { - commentIsStillEditable(comment) && - Edit - } - + /* User can edit/delete their own comment for a short window after posting */ + + { + commentIsStillEditable(comment) && + Edit + } + + } + { (currentUser && (comment.user.id !== currentUser.id)) && /* TopRightMenu allows currentUser to ignore other users' comments */ - : - - + + + } - { this.state.isEditing ? { + this.initialState = false; + this.props.loadMore(); + } + render () { - const {topLevel, moreComments, loadMore, replyCount} = this.props; + const {topLevel, moreComments, replyCount} = this.props; return moreComments ?
diff --git a/client/coral-embed-stream/src/components/Stream.js b/client/coral-embed-stream/src/components/Stream.js index 62a58129b..f6c67fd8d 100644 --- a/client/coral-embed-stream/src/components/Stream.js +++ b/client/coral-embed-stream/src/components/Stream.js @@ -53,6 +53,8 @@ function invalidateCursor(invalidated, state, props) { class Stream extends React.Component { + isLoadingMore = false; + constructor(props) { super(props); this.state = resetCursors(this.state, props); @@ -95,6 +97,18 @@ class Stream extends React.Component { } }; + loadMoreComments = () => { + if (!this.isLoadingMore) { + this.isLoadingMore = true; + this.props.loadMoreComments() + .then(() => this.isLoadingMore = false) + .catch((e) => { + this.isLoadingMore = false; + throw e; + }); + } + } + // getVisibileComments returns a list containing comments // which were authored by current user or comes after the `idCursor`. getVisibleComments() { @@ -288,7 +302,7 @@ class Stream extends React.Component {
}
diff --git a/client/coral-embed-stream/src/components/TopRightMenu.css b/client/coral-embed-stream/src/components/Toggleable.css similarity index 100% rename from client/coral-embed-stream/src/components/TopRightMenu.css rename to client/coral-embed-stream/src/components/Toggleable.css diff --git a/client/coral-embed-stream/src/components/Toggleable.js b/client/coral-embed-stream/src/components/Toggleable.js new file mode 100644 index 000000000..c7996d866 --- /dev/null +++ b/client/coral-embed-stream/src/components/Toggleable.js @@ -0,0 +1,39 @@ +import React from 'react'; +import ClickOutside from 'coral-framework/components/ClickOutside'; +import styles from './Toggleable.css'; +import classnames from 'classnames'; + +const upArrow = ; +const downArrow = ; + +export default class Toggleable extends React.Component { + constructor(props) { + super(props); + this.state = { + isOpen: false + }; + } + + toggle = () => { + this.setState({isOpen: !this.state.isOpen}); + } + + close = () => { + this.setState({isOpen: false}); + } + + render() { + const {children} = this.props; + const {isOpen} = this.state; + return ( + + + {isOpen ? upArrow : downArrow} + {isOpen ? children : null} + + + ); + } +} + diff --git a/client/coral-embed-stream/src/components/TopRightMenu.js b/client/coral-embed-stream/src/components/TopRightMenu.js index 84d64f061..d65722b6d 100644 --- a/client/coral-embed-stream/src/components/TopRightMenu.js +++ b/client/coral-embed-stream/src/components/TopRightMenu.js @@ -1,8 +1,6 @@ import React, {PropTypes} from 'react'; -import classnames from 'classnames'; - import {IgnoreUserWizard} from './IgnoreUserWizard'; -import styles from './TopRightMenu.css'; +import Toggleable from './Toggleable'; // TopRightMenu appears as a dropdown in the top right of the comment. // when you click the down cehvron, it expands and shows IgnoreUserWizard @@ -56,38 +54,6 @@ export class TopRightMenu extends React.Component { />
- ); - } -} - -const upArrow = ; -const downArrow = ; -class Toggleable extends React.Component { - constructor(props) { - super(props); - this.toggle = this.toggle.bind(this); - this.close = this.close.bind(this); - this.state = { - isOpen: false - }; - } - toggle() { - this.setState({isOpen: !this.state.isOpen}); - } - close() { - this.setState({isOpen: false}); - } - render() { - const {children} = this.props; - const {isOpen} = this.state; - return ( - - // /*onBlur={ this.close } */ - - {isOpen ? upArrow : downArrow} - {isOpen ? children : null} - ); } } diff --git a/client/coral-embed/src/index.js b/client/coral-embed/src/index.js index 14de2756e..5b42fa541 100644 --- a/client/coral-embed/src/index.js +++ b/client/coral-embed/src/index.js @@ -73,6 +73,11 @@ function configurePymParent(pymParent, opts) { window.document.body.appendChild(snackbar); + // Notify embed that there was a click outside. + document.addEventListener('click', () => { + pymParent.sendMessage('click'); + }, true); + // Workaround: IOS Safari ignores `width` but respects `min-width` value. pymParent.el.firstChild.style.width = '1px'; pymParent.el.firstChild.style.minWidth = '100%'; diff --git a/client/coral-framework/components/ClickOutside.js b/client/coral-framework/components/ClickOutside.js new file mode 100644 index 000000000..e13ef4472 --- /dev/null +++ b/client/coral-framework/components/ClickOutside.js @@ -0,0 +1,35 @@ +import {Component, cloneElement, Children} from 'react'; +import PropTypes from 'prop-types'; +import {findDOMNode} from 'react-dom'; +import {pym} from 'coral-framework'; + +export default class ClickOutside extends Component { + static propTypes = { + onClickOutside: PropTypes.func.isRequired + }; + + domNode = null; + + handleClick = (e) => { + const {onClickOutside} = this.props; + if (!e || !this.domNode.contains(e.target)) { + onClickOutside(e); + } + }; + + componentDidMount() { + this.domNode = findDOMNode(this); + document.addEventListener('click', this.handleClick, true); + pym.onMessage('click', this.handleClick); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClick, true); + pym.messageHandlers.click = pym.messageHandlers.click.filter((h) => h !== this.handleClick); + } + + render() { + const {children, onClickOutside: _, ...rest} = this.props; + return cloneElement(Children.only(children), rest); + } +} diff --git a/client/coral-plugin-flags/FlagButton.js b/client/coral-plugin-flags/FlagButton.js index 73ed50e9c..e8a17f201 100644 --- a/client/coral-plugin-flags/FlagButton.js +++ b/client/coral-plugin-flags/FlagButton.js @@ -4,11 +4,11 @@ import t from 'coral-framework/services/i18n'; import {can} from 'coral-framework/services/perms'; import {PopupMenu, Button} from 'coral-ui'; -import onClickOutside from 'react-onclickoutside'; +import ClickOutside from 'coral-framework/components/ClickOutside'; const name = 'coral-plugin-flags'; -class FlagButton extends Component { +export default class FlagButton extends Component { state = { showMenu: false, @@ -128,7 +128,7 @@ class FlagButton extends Component { this.setState({message: e.target.value}); } - handleClickOutside () { + handleClickOutside = () => { this.closeMenu(); } @@ -138,79 +138,81 @@ class FlagButton extends Component { const flagged = flaggedByCurrentUser || localPost; const popupMenu = getPopupMenu[this.state.step](this.state.itemType); - return
- - { - this.state.showMenu && -
this.popup = ref}> - -
{popupMenu.header}
+ return ( + +
+ + { + this.state.showMenu && +
this.popup = ref}> + +
{popupMenu.header}
{ - popupMenu.options.map((option) => -
- -
-
- ) + popupMenu.text && +
{popupMenu.text}
} { - this.state.reason &&
-
-