This commit is contained in:
Belen Curcio
2017-05-01 10:47:56 -03:00
31 changed files with 431 additions and 8746 deletions
+2 -1
View File
@@ -13,7 +13,8 @@ EXPOSE 5000
COPY . /usr/src/app
# Install app dependencies and build static assets.
RUN yarn install --frozen-lockfile && \
RUN yarn global add node-gyp && \
yarn install --frozen-lockfile && \
cli plugins reconcile && \
yarn build && \
yarn install --production && \
+1 -1
View File
@@ -243,7 +243,7 @@ file under the `scripts` key including:
# Setup
Once you've installed Talk (either via Docker or source), you still need to
setup the application. If you are unfamiliar with any terminoligy used in the
setup the application. If you are unfamiliar with any terminology used in the
setup process, refer to the `TERMINOLOGY.md` document.
## Via Web
+3
View File
@@ -42,6 +42,9 @@ const routes = (
<Route path='all' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
<Route path='accepted' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
<Route path='premod' components={ModerationContainer}>
<Route path=':id' components={ModerationContainer} />
</Route>
@@ -6,6 +6,13 @@ import {menuActionsMap} from '../containers/ModerationQueue/helpers/moderationQu
const ActionButton = ({type = '', status, ...props}) => {
const typeName = type.toLowerCase();
const active = ((type === 'REJECT' && status === 'REJECTED') || (type === 'APPROVE' && status === 'ACCEPTED'));
let text = menuActionsMap[type].text;
if (text === 'Approve' && active) {
text = 'Approved';
} else if (text === 'Reject' && active) {
text = 'Rejected';
}
return (
<Button
@@ -13,7 +20,7 @@ const ActionButton = ({type = '', status, ...props}) => {
cStyle={typeName}
icon={menuActionsMap[type].icon}
onClick={type === 'APPROVE' ? props.acceptComment : props.rejectComment}
>{menuActionsMap[type].text}</Button>
>{text}</Button>
);
};
@@ -4,7 +4,6 @@ import translations from 'coral-admin/src/translations.json';
import styles from './Community.css';
import Table from './Table';
import Loading from './Loading';
import {Pager, Icon} from 'coral-ui';
import EmptyCard from '../../components/EmptyCard';
@@ -29,8 +28,8 @@ const tableHeaders = [
}
];
const People = ({isFetching, commenters, searchValue, onSearchChange, ...props}) => {
const hasResults = !isFetching && !!commenters.length;
const People = ({commenters, searchValue, onSearchChange, ...props}) => {
const hasResults = !!commenters.length;
return (
<div className={styles.container}>
<div className={styles.leftColumn}>
@@ -47,7 +46,6 @@ const People = ({isFetching, commenters, searchValue, onSearchChange, ...props})
</div>
</div>
<div className={styles.mainContent}>
{ isFetching && <Loading /> }
{
hasResults
? <Table
@@ -138,6 +138,9 @@ class ModerationContainer extends Component {
case 'all':
activeTabCount = data.allCount;
break;
case 'accepted':
activeTabCount = data.acceptedCount;
break;
case 'premod':
activeTabCount = data.premodCount;
break;
@@ -155,6 +158,7 @@ class ModerationContainer extends Component {
<ModerationMenu
asset={asset}
allCount={data.allCount}
acceptedCount={data.acceptedCount}
premodCount={data.premodCount}
rejectedCount={data.rejectedCount}
flaggedCount={data.flaggedCount}
@@ -23,7 +23,7 @@ LoadMore.propTypes = {
comments: PropTypes.array.isRequired,
loadMore: PropTypes.func.isRequired,
sort: PropTypes.oneOf(['CHRONOLOGICAL', 'REVERSE_CHRONOLOGICAL']).isRequired,
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all']).isRequired,
tab: PropTypes.oneOf(['rejected', 'premod', 'flagged', 'all', 'accepted']).isRequired,
assetId: PropTypes.string,
showLoadMore: PropTypes.bool.isRequired
};
@@ -10,7 +10,7 @@ import {Link} from 'react-router';
const lang = new I18n(translations);
const ModerationMenu = (
{asset, allCount, premodCount, rejectedCount, flaggedCount, selectSort, sort}
{asset, allCount, acceptedCount, premodCount, rejectedCount, flaggedCount, selectSort, sort}
) => {
function getPath (type) {
@@ -28,6 +28,12 @@ const ModerationMenu = (
activeClassName={styles.active}>
<Icon name='question_answer' className={styles.tabIcon} /> {lang.t('modqueue.all')} <CommentCount count={allCount} />
</Link>
<Link
to={getPath('accepted')}
className={`mdl-tabs__tab ${styles.tab}`}
activeClassName={styles.active}>
<Icon name='check' className={styles.tabIcon} /> {lang.t('modqueue.approved')} <CommentCount count={acceptedCount} />
</Link>
<Link
to={getPath('premod')}
className={`mdl-tabs__tab ${styles.tab}`}
@@ -54,6 +54,19 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
},
updateQueries: {
ModQueue: (oldData) => {
const comment = oldData.all.find(c => c.id === commentId);
let accepted;
let acceptedCount = oldData.acceptedCount;
// if the comment was already in the Approved queue, don't re-add it
if (comment.status === 'ACCEPTED') {
accepted = [...oldData.accepted];
} else {
comment.status = 'ACCEPTED';
acceptedCount++;
accepted = [comment, ...oldData.accepted];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const rejected = oldData.rejected.filter(c => c.id !== commentId);
@@ -65,9 +78,11 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
...oldData,
premodCount,
flaggedCount,
acceptedCount,
rejectedCount,
premod,
flagged,
accepted,
rejected,
};
}
@@ -82,21 +97,35 @@ export const setCommentStatus = graphql(SET_COMMENT_STATUS, {
},
updateQueries: {
ModQueue: (oldData) => {
const comment = oldData.premod.concat(oldData.flagged).filter(c => c.id === commentId)[0];
const rejected = [comment].concat(oldData.rejected);
const comment = oldData.all.find(c => c.id === commentId);
let rejected;
let rejectedCount = oldData.rejectedCount;
// if the item was already in the Rejected queue, don't put it in again
if (comment.status === 'REJECTED') {
rejected = oldData.rejected;
} else {
comment.status = 'REJECTED';
rejectedCount++;
rejected = [comment, ...oldData.rejected];
}
const premod = oldData.premod.filter(c => c.id !== commentId);
const flagged = oldData.flagged.filter(c => c.id !== commentId);
const accepted = oldData.accepted.filter(c => c.id !== commentId);
const premodCount = premod.length < oldData.premod.length ? oldData.premodCount - 1 : oldData.premodCount;
const flaggedCount = flagged.length < oldData.flagged.length ? oldData.flaggedCount - 1 : oldData.flaggedCount;
const rejectedCount = oldData.rejectedCount + 1;
const acceptedCount = accepted.length < oldData.accepted.length ? oldData.acceptedCount - 1 : oldData.acceptedCount;
return {
...oldData,
premodCount,
flaggedCount,
acceptedCount,
rejectedCount,
premod,
flagged,
accepted,
rejected
};
}
@@ -39,6 +39,9 @@ export const loadMore = (fetchMore) => ({limit, cursor, sort, tab, asset_id}) =>
case 'all':
statuses = null;
break;
case 'accepted':
statuses = ['ACCEPTED'];
break;
case 'premod':
statuses = ['PREMOD'];
break;
@@ -8,6 +8,13 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
}) {
...commentView
}
accepted: comments(query: {
statuses: [ACCEPTED],
asset_id: $asset_id,
sort: $sort
}) {
...commentView
}
premod: comments(query: {
statuses: [PREMOD],
asset_id: $asset_id,
@@ -38,6 +45,10 @@ query ModQueue ($asset_id: ID, $sort: SORT_ORDER) {
allCount: commentCount(query: {
asset_id: $asset_id
})
acceptedCount: commentCount(query: {
statuses: [ACCEPTED],
asset_id: $asset_id
})
premodCount: commentCount(query: {
statuses: [PREMOD],
asset_id: $asset_id
@@ -7,6 +7,32 @@ const fm = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: 'INTERFACE',
name: 'UserError',
possibleTypes: [
{name: 'GenericUserError'},
{name: 'ValidationUserError'}
]
},
{
kind: 'INTERFACE',
name: 'Response',
possibleTypes: [
{name: 'CreateCommentResponse'},
{name: 'CreateLikeResponse'},
{name: 'CreateFlagResponse'},
{name: 'CreateDontAgreeResponse'},
{name: 'DeleteActionResponse'},
{name: 'SetUserStatusResponse'},
{name: 'SuspendUserResponse'},
{name: 'SetCommentStatusResponse'},
{name: 'AddCommentTagResponse'},
{name: 'RemoveCommentTagResponse'},
{name: 'IgnoreUserResponse'},
{name: 'StopIgnoringUserResponse'}
]
},
{
kind: 'INTERFACE',
name: 'Action',
@@ -24,6 +50,15 @@ const fm = new IntrospectionFragmentMatcher({
{name: 'LikeActionSummary'},
{name: 'DontAgreeActionSummary'}
],
},
{
kind: 'INTERFACE',
name: 'AssetActionSummary',
possibleTypes: [
{name: 'DefaultAssetActionSummary'},
{name: 'FlagAssetActionSummary'},
{name: 'LikeAssetActionSummary'}
]
}
],
},
+3
View File
@@ -36,6 +36,7 @@
"modqueue": {
"likes": "likes",
"all": "all",
"approved": "approved",
"premod": "pre-mod",
"rejected": "rejected",
"flagged": "flagged",
@@ -227,6 +228,8 @@
"loading": "Cargando resultados"
},
"modqueue": {
"all": "todos",
"approved": "aprobado",
"likes": "gustos",
"premod": "pre-mod",
"rejected": "rechazado",
@@ -33,12 +33,14 @@ export default class Embed extends React.Component {
}
}
handleShowProfile = () => this.props.setActiveTab('profile');
render () {
const {activeTab, logout, viewAllComments, commentId} = this.props;
const {asset: {totalCommentCount}} = this.props.root;
const {loggedIn, isAdmin, user} = this.props.auth;
const userBox = <UserBox user={user} logout={logout} changeTab={this.changeTab}/>;
const userBox = <UserBox user={user} onLogout={logout} onShowProfile={this.handleShowProfile}/>;
return (
<div>
@@ -3,6 +3,8 @@ import {compose, gql, graphql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import isEqual from 'lodash/isEqual';
import branch from 'recompose/branch';
import renderComponent from 'recompose/renderComponent';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
@@ -19,7 +21,6 @@ class EmbedContainer extends React.Component {
componentDidMount() {
pym.sendMessage('childReady');
this.props.checkLogin();
}
componentWillReceiveProps(nextProps) {
@@ -108,6 +109,10 @@ const mapDispatchToProps = dispatch =>
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(
props => !props.auth.checkedInitialLogin,
renderComponent(Spinner),
),
withQuery,
)(EmbedContainer);
@@ -19,24 +19,13 @@ const {showSignInDialog} = authActions;
const {addNotification} = notificationActions;
class StreamContainer extends React.Component {
getCounts = ({asset_id, limit, sort}) => {
getCounts = (variables) => {
return this.props.data.fetchMore({
query: LOAD_COMMENT_COUNTS_QUERY,
variables: {
asset_id,
limit,
sort,
excludeIgnored: this.props.data.variables.excludeIgnored,
},
updateQuery: (oldData, {fetchMoreResult:{asset}}) => {
return {
...oldData,
asset: {
...oldData.asset,
commentCount: asset.commentCount
}
};
}
variables,
// Apollo requires this, even though we don't use it...
updateQuery: data => data,
});
};
@@ -118,14 +107,11 @@ class StreamContainer extends React.Component {
};
componentDidMount() {
this.props.data.refetch();
if (this.props.previousTab) {
this.props.data.refetch();
}
this.countPoll = setInterval(() => {
const {asset} = this.props.root;
this.getCounts({
asset_id: asset.id,
limit: asset.comments.length,
sort: 'REVERSE_CHRONOLOGICAL'
});
this.getCounts(this.props.data.variables);
}, NEW_COMMENT_COUNT_POLL_INTERVAL);
}
@@ -139,13 +125,13 @@ class StreamContainer extends React.Component {
}
const LOAD_COMMENT_COUNTS_QUERY = gql`
query LoadCommentCounts($asset_id: ID, $limit: Int = 5, $sort: SORT_ORDER) {
asset(id: $asset_id) {
query LoadCommentCounts($assetUrl: String, $assetId: ID, $excludeIgnored: Boolean) {
asset(id: $assetId, url: $assetUrl) {
id
commentCount
comments(sort: $sort, limit: $limit) {
commentCount(excludeIgnored: $excludeIgnored)
comments(limit: 10) {
id
replyCount
replyCount(excludeIgnored: $excludeIgnored)
}
}
}
@@ -236,6 +222,7 @@ const mapStateToProps = state => ({
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
previousTab: state.embed.previousTab,
});
const mapDispatchToProps = dispatch =>
+6
View File
@@ -3,6 +3,7 @@ import {render} from 'react-dom';
import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import {checkLogin} from 'coral-framework/actions/auth';
import reducers from './reducers';
import localStore, {injectReducers} from 'coral-framework/services/store';
@@ -12,6 +13,11 @@ injectReducers(reducers);
const store = (window.opener && window.opener.coralStore) ? window.opener.coralStore : localStore;
// Don't run this in the popup.
if (store === localStore) {
store.dispatch(checkLogin());
}
render(
<ApolloProvider client={client} store={store}>
<AppRouter />
@@ -2,6 +2,7 @@ import * as actions from '../constants/embed';
const initialState = {
activeTab: 'stream',
previousTab: '',
};
export default function stream(state = initialState, action) {
@@ -10,6 +11,7 @@ export default function stream(state = initialState, action) {
return {
...state,
activeTab: action.tab,
previousTab: state.activeTab,
};
default:
return state;
+1 -11
View File
@@ -352,17 +352,6 @@ button.comment__action-button[disabled],
/* Flag Styles */
.coral-plugin-flags-container {
position: relative;
}
.coral-plugin-flags-popup span {
min-width: 280px;
bottom: 36px;
position: absolute;
right: 10px;
}
.coral-plugin-flags-popup-form {
margin-bottom: 10px;
}
@@ -399,6 +388,7 @@ button.comment__action-button[disabled],
margin-top: 5px;
width: 75%;
font-size: 16px;
border: 1px solid #ccc;
}
/* Close comments */
+21 -4
View File
@@ -39,10 +39,21 @@ export const showSignInDialog = () => dispatch => {
'menubar=0,resizable=0,width=500,height=550,top=200,left=500'
);
signInPopUp.onbeforeunload = () => {
dispatch(checkLogin());
fetchMe();
// Workaround odd behavior in older WebKit versions, where
// onunload is called twice. (Encountered in IOS 8.3)
let loaded = false;
signInPopUp.onload = () => {
loaded = true;
};
// Use `onunload` instead of `onbeforeunload` which is not supported in IOS Safari.
signInPopUp.onunload = () => {
if (loaded) {
dispatch(checkLogin());
fetchMe();
}
};
dispatch({type: actions.SHOW_SIGNIN_DIALOG});
};
export const hideSignInDialog = () => dispatch => {
@@ -177,7 +188,13 @@ export const fetchSignUp = (formData, redirectUri) => (dispatch) => {
dispatch(signUpSuccess(user));
})
.catch(error => {
dispatch(signUpFailure(lang.t(`error.${error.message}`)));
let errorMessage = lang.t(`error.${error.message}`);
// if there is no translation defined, just show the error string
if (errorMessage === `error.${error.message}`) {
errorMessage = error.message;
}
dispatch(signUpFailure(errorMessage));
});
};
@@ -57,7 +57,7 @@ export const postComment = graphql(POST_COMMENT, {
...oldData.asset,
comments: oldData.asset.comments.map((oldComment) => {
return oldComment.id === parent_id
? {...oldComment, replies: [...oldComment.replies, comment]}
? {...oldComment, replies: [...oldComment.replies, comment], replyCount: oldComment.replyCount + 1}
: oldComment;
})
}
+2 -2
View File
@@ -65,10 +65,10 @@ export function getSlotsFragments(slots) {
const fragments = getComponentFragments(components);
return {
spreads(key) {
return fragments[key] && fragments[key].spreads;
return (fragments[key] && fragments[key].spreads) || '';
},
definitions(key) {
return fragments[key] && fragments[key].definitions;
return (fragments[key] && fragments[key].definitions) || '';
},
};
}
+8 -1
View File
@@ -8,6 +8,7 @@ const initialState = Map({
user: null,
showSignInDialog: false,
showCreateUsernameDialog: false,
checkedInitialLogin: false,
view: 'SIGNIN',
error: '',
passwordRequestSuccess: null,
@@ -71,10 +72,12 @@ export default function auth (state = initialState, action) {
.set('isLoading', true);
case actions.CHECK_LOGIN_FAILURE:
return state
.set('checkedInitialLogin', true)
.set('loggedIn', false)
.set('user', null);
case actions.CHECK_LOGIN_SUCCESS:
return state
.set('checkedInitialLogin', true)
.set('loggedIn', true)
.set('isAdmin', action.isAdmin)
.set('user', purge(action.user));
@@ -114,7 +117,11 @@ export default function auth (state = initialState, action) {
.set('isLoading', false)
.set('successSignUp', true);
case actions.LOGOUT_SUCCESS:
return initialState;
return state
.set('user', null)
.set('isLoading', false)
.set('loggedIn', false)
.set('isAdmin', false);
case actions.INVALID_FORM:
return state
.set('error', action.error);
+11 -2
View File
@@ -19,6 +19,12 @@ class FlagButton extends Component {
localDelete: false
}
componentDidUpdate () {
if (this.popup) { // this will be defined when the reporting popup is opened
this.popup.firstChild.style.top = `${this.flagButton.offsetTop - this.popup.firstChild.clientHeight - 15}px`;
}
}
// When the "report" button is clicked expand the menu
onReportClick = () => {
const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props;
@@ -135,7 +141,10 @@ class FlagButton extends Component {
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
<button onClick={!this.props.banned ? this.onReportClick : null} className={`${name}-button`}>
<button
ref={ref => this.flagButton = ref}
onClick={!this.props.banned ? this.onReportClick : null}
className={`${name}-button`}>
{
flagged
? <span className={`${name}-button-text`}>{lang.t('reported')}</span>
@@ -147,7 +156,7 @@ class FlagButton extends Component {
</button>
{
this.state.showMenu &&
<div className={`${name}-popup`}>
<div className={`${name}-popup`} ref={ref => this.popup = ref}>
<PopupMenu>
<div className={`${name}-popup-header`}>{popupMenu.header}</div>
{
+3 -3
View File
@@ -4,11 +4,11 @@ import I18n from 'coral-framework/modules/i18n/i18n';
import translations from '../translations';
const lang = new I18n(translations);
const UserBox = ({className, user, logout, changeTab}) => (
const UserBox = ({className, user, onLogout, onShowProfile}) => (
<div className={`${styles.userBox} ${className ? className : ''}`}>
{lang.t('signIn.loggedInAs')}
<a onClick={() => changeTab(1)}>{user.username}</a>. {lang.t('signIn.notYou')}
<a className={styles.logout} onClick={logout} id='logout'>{lang.t('signIn.logout')}</a>
<a onClick={onShowProfile}>{user.username}</a>. {lang.t('signIn.notYou')}
<a className={styles.logout} onClick={onLogout} id='logout'>{lang.t('signIn.logout')}</a>
</div>
);
+7 -3
View File
@@ -1,13 +1,17 @@
.popupMenu {
display: inline-block;
width: inherit;
white-space: normal;
display: block;
position: absolute;
max-width: 98%;
min-width: 50%;
border: solid 1px #999;
box-shadow: 3px 3px 5px 0 rgba(0, 0, 0, 0.3);
box-sizing: border-box;
background: white;
border-radius: 3px;
padding: 20px 10px;
z-index: 3;
z-index: 300;
right: 1%;
}
.popupMenu:before{
+1 -1
View File
@@ -2,5 +2,5 @@ import React from 'react';
import styles from './PopupMenu.css';
export default ({children}) => (
<span className={styles.popupMenu}>{children}</span>
<div className={styles.popupMenu}>{children}</div>
);
+1 -1
View File
@@ -106,7 +106,7 @@ class ErrAuthentication extends APIError {
// ErrContainsProfanity is returned in the event that the middleware detects
// profanity/wordlisted words in the payload.
const ErrContainsProfanity = new APIError('Suspected profanity. If you think this in error, please let us know!', {
const ErrContainsProfanity = new APIError('This username contains elements which are not permitted in our community. If you think this is in error, please contact us or try again.', {
translation_key: 'PROFANITY_ERROR',
status: 400
});
View File
+6 -2
View File
@@ -96,11 +96,15 @@
"prop-types": "^15.5.8",
"react-apollo": "^1.1.0",
"react-recaptcha": "^2.2.6",
"recompose": "^0.23.1",
"redis": "^2.7.1",
"uuid": "^3.0.1",
"simplemde": "^1.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"resolve": "^1.3.2",
"semver": "^5.3.0"
"semver": "^5.3.0",
"simplemde": "^1.11.2",
"uuid": "^3.0.1"
},
"devDependencies": {
"apollo-client": "^1.0.4",
@@ -179,8 +183,8 @@
"redux-thunk": "^2.1.0",
"regenerator": "^0.8.46",
"selenium-standalone": "^5.11.2",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"style-loader": "^0.16.0",
"subscriptions-transport-ws": "^0.5.5-alpha.0",
"supertest": "^2.0.1",
"timeago.js": "^2.0.3",
"webpack": "^2.3.1"
+225 -8673
View File
File diff suppressed because it is too large Load Diff