Merge branch 'master' into french-translation

This commit is contained in:
Kim Gardner
2017-07-05 09:17:36 +01:00
committed by GitHub
313 changed files with 8149 additions and 4561 deletions
@@ -1,8 +1,7 @@
import React from 'react';
import styles from './styles.css';
import {Dialog, Alert, TextField} from 'coral-ui';
import {Dialog, Alert, TextField, Button} from 'plugin-api/beta/client/components/ui';
import {FakeComment} from './FakeComment';
import Button from 'coral-ui/components/Button';
import t from 'coral-framework/services/i18n';
const CreateUsernameDialog = ({
@@ -33,7 +32,7 @@ const CreateUsernameDialog = ({
className={styles.fakeComment}
username={formData.username}
created_at={Date.now()}
body={t('createdisplay.fake_comment_body')}
comment={{body:t('createdisplay.fake_comment_body')}}
/>
<p className={styles.ifyoudont}>
{t('createdisplay.if_you_dont_change_your_name')}
@@ -1,17 +1,17 @@
import React from 'react';
import t from 'coral-framework/services/i18n';
import {ReplyButton} from 'coral-plugin-replies';
import PubDate from 'coral-plugin-pubdate/PubDate';
import Slot from 'coral-framework/components/Slot';
import AuthorName from 'coral-plugin-author-name/AuthorName';
import Content from 'coral-plugin-commentcontent/CommentContent';
import styles from 'coral-embed-stream/src/components/Comment.css';
import t from 'coral-framework/services/i18n';
export const FakeComment = ({username, created_at, body}) => (
export const FakeComment = ({username, created_at, comment}) => (
<div className={`comment ${styles.Comment}`} style={{marginLeft: 0 * 30}}>
<hr aria-hidden={true} />
<AuthorName author={{name: username}} />
<PubDate created_at={created_at} />
<Content body={body} />
<Slot comment={comment} />
<div className="commentActionsLeft comment__action-container">
<div className={`${'coral-plugin-likes'}-container`}>
<button className={'coral-plugin-likes-button'}>
@@ -30,7 +30,6 @@ export const FakeComment = ({username, created_at, body}) => (
onClick={() => {}}
parentCommentId={'commentID'}
currentUserId={{}}
banned={false}
/>
</div>
<div className="commentActionsRight comment__action-container">
@@ -1,6 +1,6 @@
import React from 'react';
import styles from './styles.css';
import Button from 'coral-ui/components/Button';
import {Button} from 'plugin-api/beta/client/components/ui';
import t from 'coral-framework/services/i18n';
class ForgotContent extends React.Component {
@@ -1,5 +1,5 @@
import React from 'react';
import {Dialog} from 'coral-ui';
import {Dialog} from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
import SignInContent from './SignInContent';
@@ -1,5 +1,5 @@
import React from 'react';
import {Button} from 'coral-ui';
import {Button} from 'plugin-api/beta/client/components/ui';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {showSignInDialog} from 'coral-framework/actions/auth';
@@ -1,5 +1,5 @@
import React, {PropTypes} from 'react';
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
@@ -1,6 +1,6 @@
import styles from './styles.css';
import React from 'react';
import {Button, TextField, Spinner, Success, Alert} from 'coral-ui';
import {Button, TextField, Spinner, Success, Alert} from 'plugin-api/beta/client/components/ui';
import t from 'coral-framework/services/i18n';
class SignUpContent extends React.Component {
@@ -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,24 @@
import React from 'react';
import Linkify from 'react-linkify';
const name = 'coral-plugin-comment-content';
const CommentContent = ({comment}) => {
const textbreaks = comment.body.split('\n');
return <div className={`${name}-text`}>
{
textbreaks.map((line, i) => {
return (
<span key={i} className={`${name}-line`}>
<Linkify properties={{target: '_blank'}}>
{line}
</Linkify>
<br className={`${name}-linebreak`}/>
</span>
);
})
}
</div>;
};
export default CommentContent;
@@ -0,0 +1,7 @@
import CommentContent from './components/CommentContent';
export default {
slots: {
commentContent: [CommentContent]
}
};
@@ -0,0 +1 @@
module.exports = {};
@@ -0,0 +1,14 @@
{
"name": "coral-plugin-comment-content",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"react-linkify": "^0.2.1"
}
}
@@ -0,0 +1,112 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
asap@~2.0.3:
version "2.0.5"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.5.tgz#522765b50c3510490e52d7dcfe085ef9ba96958f"
core-js@^1.0.0:
version "1.2.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
encoding@^0.1.11:
version "0.1.12"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb"
dependencies:
iconv-lite "~0.4.13"
fbjs@^0.8.9:
version "0.8.12"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04"
dependencies:
core-js "^1.0.0"
isomorphic-fetch "^2.1.1"
loose-envify "^1.0.0"
object-assign "^4.1.0"
promise "^7.1.1"
setimmediate "^1.0.5"
ua-parser-js "^0.7.9"
iconv-lite@~0.4.13:
version "0.4.18"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2"
is-stream@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
isomorphic-fetch@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
dependencies:
node-fetch "^1.0.1"
whatwg-fetch ">=0.10.0"
js-tokens@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
linkify-it@^1.2.0:
version "1.2.4"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-1.2.4.tgz#0773526c317c8fd13bd534ee1d180ff88abf881a"
dependencies:
uc.micro "^1.0.1"
loose-envify@^1.0.0, loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
js-tokens "^3.0.0"
node-fetch@^1.0.1:
version "1.7.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.1.tgz#899cb3d0a3c92f952c47f1b876f4c8aeabd400d5"
dependencies:
encoding "^0.1.11"
is-stream "^1.0.1"
object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
promise@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.1.1.tgz#489654c692616b8aa55b0724fa809bb7db49c5bf"
dependencies:
asap "~2.0.3"
prop-types@^15.5.8:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
react-linkify@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/react-linkify/-/react-linkify-0.2.1.tgz#b28d3f9544539a622fec8d42b4800eb9d23bf981"
dependencies:
linkify-it "^1.2.0"
prop-types "^15.5.8"
tlds "^1.57.0"
setimmediate@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
tlds@^1.57.0:
version "1.189.0"
resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.189.0.tgz#b8cb46ea76dc2f4a01d45b8d907bf19a66e9f729"
ua-parser-js@^0.7.9:
version "0.7.12"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
uc.micro@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"
whatwg-fetch@>=0.10.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84"
@@ -0,0 +1,57 @@
import React from 'react';
import styles from './styles.css';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t, can} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const plugin = 'coral-plugin-like';
class LikeButton extends React.Component {
handleClick = () => {
const {
postReaction,
deleteReaction,
showSignInDialog,
addNotification,
alreadyReacted,
user,
} = this.props;
// If the current user does not exist, trigger sign in dialog.
if (!user) {
showSignInDialog();
return;
}
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
if (alreadyReacted) {
deleteReaction();
} else {
postReaction();
}
};
render() {
const {count, alreadyReacted} = this.props;
return (
<div className={cn(styles.container, `${plugin}-container`)}>
<button
className={cn(styles.button, {[styles.liked]: alreadyReacted}, `${plugin}-button`)}
onClick={this.handleClick}
>
<span>{t(alreadyReacted ? 'coral-plugin-like.liked' : 'coral-plugin-like.like')}</span>
<Icon name="thumb_up" className={`${plugin}-icon`} />
<span className={`${plugin}-count`}>{count > 0 && count}</span>
</button>
</div>
);
}
}
export default withReaction('like')(LikeButton);
@@ -1,86 +0,0 @@
import React, {Component} from 'react';
import styles from './style.css';
import cn from 'classnames';
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
import t from 'coral-framework/services/i18n';
const name = 'coral-plugin-like';
class LikeButton extends Component {
handleClick = () => {
const {postLike, showSignInDialog, deleteAction} = this.props;
const {root: {me}, comment} = this.props;
const myLikeActionSummary = getMyActionSummary(
'LikeActionSummary',
comment
);
// If the current user does not exist, trigger sign in dialog.
if (!me) {
showSignInDialog();
return;
}
// If the current user is banned, do nothing.
if (me.status === 'BANNED') {
return;
}
if (myLikeActionSummary) {
deleteAction(myLikeActionSummary.current_user.id, comment.id);
} else {
postLike({
item_id: comment.id,
item_type: 'COMMENTS'
});
}
};
render() {
const {comment} = this.props;
if (!comment) {
return null;
}
const myLike = getMyActionSummary('LikeActionSummary', comment);
let count = getTotalActionCount('LikeActionSummary', comment);
return (
<div className={cn(styles.like, `${name}-container`)}>
<button
className={cn(
styles.button,
{[styles.liked]: myLike},
`${name}-button`
)}
onClick={this.handleClick}
>
<span className={`${name}-button-text`}>
{t(myLike ? 'liked' : 'like')}
</span>
<i
className={cn(
styles.icon,
'material-icons',
{[styles.liked]: myLike},
`${name}-icon`
)}
aria-hidden={true}
>
thumb_up
</i>
<span className={`${name}-count`}>{count > 0 && count}</span>
</button>
</div>
);
}
}
LikeButton.propTypes = {
data: React.PropTypes.object.isRequired
};
export default LikeButton;
@@ -1,186 +0,0 @@
import get from 'lodash/get';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {compose, gql, graphql} from 'react-apollo';
import LikeButton from '../components/LikeButton';
import withFragments from 'coral-framework/hocs/withFragments';
import{showSignInDialog} from 'coral-framework/actions/auth';
const isLikeAction = (a) => a.__typename === 'LikeActionSummary';
const COMMENT_FRAGMENT = gql`
fragment LikeButton_updateFragment on Comment {
action_summaries {
... on LikeActionSummary {
count
current_user {
id
}
}
}
}
`;
const withDeleteAction = graphql(
gql`
mutation deleteAction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
`,
{
props: ({mutate}) => ({
deleteAction: (id, commentId) => {
return mutate({
variables: {id},
optimisticResponse: {
deleteAction: {
__typename: 'DeleteActionResponse',
errors: null
}
},
update: (proxy) => {
const fragmentId = `Comment_${commentId}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Check whether we liked this comment.
const idx = data.action_summaries.findIndex(isLikeAction);
if (
idx < 0 ||
get(data.action_summaries[idx], 'current_user.id') !== id
) {
return;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count - 1,
current_user: null
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const withPostLike = graphql(
gql`
mutation createLike($like: CreateLikeInput!) {
createLike(like: $like) {
like {
id
}
errors {
translation_key
}
}
}
`,
{
props: ({mutate}) => ({
postLike: (like) => {
return mutate({
variables: {like},
optimisticResponse: {
createLike: {
__typename: 'CreateLikeResponse',
errors: null,
like: {
__typename: 'LikeAction',
id: 'pending'
}
}
},
update: (proxy, mutationResult) => {
const fragmentId = `Comment_${like.item_id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId
});
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isLikeAction);
// Check whether we already liked this comment.
if (idx >= 0 && data.action_summaries[idx].current_user) {
return;
}
if (idx < 0) {
// Add initial action when it doesn't exist.
data.action_summaries.push({
__typename: 'LikeActionSummary',
count: 0,
current_user: null
});
idx = data.action_summaries.length - 1;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count + 1,
current_user: mutationResult.data.createLike.like
};
// Write our data back to the cache.
proxy.writeFragment({
fragment: COMMENT_FRAGMENT,
id: fragmentId,
data
});
}
});
}
})
}
);
const mapDispatchToProps = (dispatch) =>
bindActionCreators({showSignInDialog}, dispatch);
const enhance = compose(
withFragments({
root: gql`
fragment LikeButton_root on RootQuery {
me {
status
}
}
`,
comment: gql`
fragment LikeButton_comment on Comment {
action_summaries {
... on LikeActionSummary {
count
current_user {
id
}
}
}
}`
}),
connect(null, mapDispatchToProps),
withDeleteAction,
withPostLike
);
export default enhance(LikeButton);
+2 -2
View File
@@ -1,5 +1,5 @@
import LikeButton from './containers/LikeButton';
import translations from './translations.json';
import LikeButton from './LikeButton';
import translations from './translations.yml';
export default {
translations,
@@ -1,6 +1,6 @@
.like {
.container {
display: inline-block;
}
}
.button {
color: #2a2a2a;
@@ -17,14 +17,9 @@
&.liked {
color: rgb(0,134,227);
&:hover {
color: rgb(0,134,227);
cursor: pointer;
}
}
}
.icon {
padding: 0 5px;
}
@@ -1,10 +0,0 @@
{
"en": {
"like": "Like",
"liked": "Liked"
},
"es": {
"like": "Me Gusta",
"liked": "Me Gustó"
}
}
@@ -0,0 +1,9 @@
en:
coral-plugin-like:
like: Like
liked: Liked
es:
coral-plugin-like:
like: Me Gusta
liked: Me Gustó
+2 -36
View File
@@ -1,36 +1,2 @@
const {readFileSync} = require('fs');
const path = require('path');
const wrapResponse = require('../../graph/helpers/response');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
resolvers: {
RootMutation: {
createLike(_, {like: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('like')(Action.create({item_id, item_type, action_type: 'LIKE'}));
}
}
},
hooks: {
Action: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LIKE':
return 'LikeAction';
}
}
}
},
ActionSummary: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LIKE':
return 'LikeActionSummary';
}
}
}
}
}
};
const {getReactionConfig} = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('like');
@@ -1,70 +0,0 @@
enum ACTION_TYPE {
# Represents a Like.
LIKE
}
enum ASSET_METRICS_SORT {
# Represents a LikeAction.
LIKE
}
input CreateLikeInput {
# The item's id for which we are to create a like.
item_id: ID!
# The type of the item for which we are to create the like.
item_type: ACTION_ITEM_TYPE!
}
# LikeAction is used by users who "like" a specific entity.
type LikeAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
type LikeActionSummary implements ActionSummary {
# The count of actions with this group.
count: Int
# The current user's action.
current_user: LikeAction
}
# A summary of counts related to all the Likes on an Asset.
type LikeAssetActionSummary implements AssetActionSummary {
# Number of likes associated with actionable types on this this Asset.
actionCount: Int
# Number of unique actionable types that are referenced by the likes.
actionableItemCount: Int
}
type CreateLikeResponse implements Response {
# The like that was created.
like: LikeAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
type RootMutation {
# Creates a like on an entity.
createLike(like: CreateLikeInput!): CreateLikeResponse
}
+23 -16
View File
@@ -1,8 +1,11 @@
import React from 'react';
import {Icon} from 'coral-ui';
import styles from './styles.css';
import t from 'coral-framework/services/i18n';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t, can} from 'plugin-api/beta/client/services';
import {Icon} from 'plugin-api/beta/client/components/ui';
import cn from 'classnames';
const plugin = 'coral-plugin-love';
class LoveButton extends React.Component {
handleClick = () => {
@@ -10,22 +13,24 @@ class LoveButton extends React.Component {
postReaction,
deleteReaction,
showSignInDialog,
alreadyReacted
addNotification,
alreadyReacted,
user,
} = this.props;
const {root: {me}} = this.props;
// If the current user does not exist, trigger sign in dialog.
if (!me) {
if (!user) {
showSignInDialog();
return;
}
// If the current user is banned, do nothing.
if (me.status === 'BANNED') {
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
if (alreadyReacted()) {
if (alreadyReacted) {
deleteReaction();
} else {
postReaction();
@@ -35,14 +40,16 @@ class LoveButton extends React.Component {
render() {
const {count, alreadyReacted} = this.props;
return (
<button
className={`${styles.button} ${alreadyReacted() ? styles.loved : ''}`}
onClick={this.handleClick}
>
<span>{t(alreadyReacted() ? 'loved' : 'love')}</span>
<Icon name="favorite" />
<span>{count > 0 && count}</span>
</button>
<div className={cn(styles.container, `${plugin}-container`)}>
<button
className={cn(styles.button, {[styles.loved]: alreadyReacted}, `${plugin}-button`)}
onClick={this.handleClick}
>
<span>{t(alreadyReacted ? 'coral-plugin-love.loved' : 'coral-plugin-love.love')}</span>
<Icon name="favorite" className={`${plugin}-icon`} />
<span className={`${plugin}-count`}>{count > 0 && count}</span>
</button>
</div>
);
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
import LoveButton from './LoveButton';
import translations from './translations.json';
import translations from './translations.yml';
export default {
translations,
+18 -19
View File
@@ -1,26 +1,25 @@
.respect {
display: inline-block;
.container {
display: inline-block;
}
.button {
color: #2a2a2a;
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
border: none;
font-size: inherit;
color: #2a2a2a;
margin: 5px 10px 5px 0px;
background: none;
padding: 0px;
border: none;
font-size: inherit;
&:hover {
color: #767676;
cursor: pointer;
}
&.loved {
color: #e52338;
&:hover {
color: #767676;
cursor: pointer;
}
&.loved {
color: #e52338;
&:hover {
color: #e52839;
cursor: pointer;
}
color: #e52839;
cursor: pointer;
}
}
}
@@ -1,10 +0,0 @@
{
"en": {
"love": "Love",
"loved": "Loved"
},
"es": {
"love": "Amo",
"loved": "Amé"
}
}
@@ -0,0 +1,9 @@
en:
coral-plugin-love:
love: Love
loved: Loved
es:
coral-plugin-love:
love: Amo
loved: Amé
+2 -36
View File
@@ -1,36 +1,2 @@
const {readFileSync} = require('fs');
const path = require('path');
const wrapResponse = require('../../graph/helpers/response');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8'),
resolvers: {
RootMutation: {
createLove(_, {love: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('love')(Action.create({item_id, item_type, action_type: 'LOVE'}));
}
}
},
hooks: {
Action: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LOVE':
return 'LoveAction';
}
}
}
},
ActionSummary: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'LOVE':
return 'LoveActionSummary';
}
}
}
}
}
};
const {getReactionConfig} = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('love');
@@ -1,70 +0,0 @@
enum ACTION_TYPE {
# Represents a Love.
LOVE
}
enum ASSET_METRICS_SORT {
# Represents a LoveAction.
LOVE
}
input CreateLoveInput {
# The item's id for which we are to create a love.
item_id: ID!
# The type of the item for which we are to create the love.
item_type: ACTION_ITEM_TYPE!
}
# LoveAction is used by users who "love" a specific entity.
type LoveAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
type LoveActionSummary implements ActionSummary {
# The count of actions with this group.
count: Int
# The current user's action.
current_user: LoveAction
}
# A summary of counts related to all the Loves on an Asset.
type LoveAssetActionSummary implements AssetActionSummary {
# Number of loves associated with actionable types on this this Asset.
actionCount: Int
# Number of unique actionable types that are referenced by the loves.
actionableItemCount: Int
}
type CreateLoveResponse implements Response {
# The love that was created.
love: LoveAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
type RootMutation {
# Creates a love on an entity.
createLove(love: CreateLoveInput!): CreateLoveResponse
}
@@ -1,6 +1,6 @@
import React from 'react';
import Box from './Box';
import {Button} from 'coral-ui';
import {Button} from 'plugin-api/beta/client/components/ui';
import styles from './styles.css';
export default class Footer extends React.Component {
@@ -1,11 +1,8 @@
import React from 'react';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
const isOffTopic = (tags) => {
return !!tags.filter((tag) => tag.name === 'OFF_TOPIC').length;
};
const isOffTopic = (tags) => !!tags.filter((t) => t.tag.name === 'OFF_TOPIC').length;
export default (props) => (
<span>
+12 -4
View File
@@ -1,6 +1,14 @@
const {readFileSync} = require('fs');
const path = require('path');
module.exports = {
typeDefs: readFileSync(path.join(__dirname, 'server/typeDefs.graphql'), 'utf8')
tags: [
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
};
@@ -1,4 +0,0 @@
## Extending TAG_TYPE by adding OFF_TOPIC Tag
enum TAG_TYPE {
OFF_TOPIC
}
@@ -0,0 +1,59 @@
import React from 'react';
import Icon from './Icon';
import styles from './styles.css';
import {withReaction} from 'plugin-api/beta/client/hocs';
import {t, can} from 'plugin-api/beta/client/services';
import cn from 'classnames';
const plugin = 'coral-plugin-respect';
class RespectButton extends React.Component {
handleClick = () => {
const {
postReaction,
deleteReaction,
showSignInDialog,
alreadyReacted,
addNotification,
user,
} = this.props;
// If the current user does not exist, trigger sign in dialog.
if (!user) {
showSignInDialog();
return;
}
// If the current user is suspended, do nothing.
if (!can(user, 'INTERACT_WITH_COMMUNITY')) {
addNotification('error', t('error.NOT_AUTHORIZED'));
return;
}
if (alreadyReacted) {
deleteReaction();
} else {
postReaction();
}
};
render() {
const {count, alreadyReacted} = this.props;
return (
<div className={cn(styles.container, `${plugin}-container`)}>
<button
className={cn(styles.button, {[styles.respected]: alreadyReacted}, `${plugin}-button`)}
onClick={this.handleClick}
>
<span className={`${plugin}-button-text`}>
{t(alreadyReacted ? 'coral-plugin-respect.respected' : 'coral-plugin-respect.respect')}
</span>
<Icon className={cn(styles.icon, `${plugin}-icon`)} />
<span className={cn(styles.icon, `${plugin}-count`)}>{count > 0 && count}</span>
</button>
</div>
);
}
}
export default withReaction('respect')(RespectButton);
@@ -1,6 +0,0 @@
import React from 'react';
import cn from 'classnames';
export default ({className}) => (
<i className={cn('fa', 'fa-handshake-o', className)} aria-hidden="true"/>
);
@@ -1,69 +0,0 @@
import React, {Component} from 'react';
import styles from './style.css';
import Icon from './Icon';
import t from 'coral-framework/services/i18n';
import cn from 'classnames';
import {getMyActionSummary, getTotalActionCount} from 'coral-framework/utils';
const name = 'coral-plugin-respect';
class RespectButton extends Component {
handleClick = () => {
const {postRespect, showSignInDialog, deleteAction} = this.props;
const {root: {me}, comment} = this.props;
const myRespectActionSummary = getMyActionSummary('RespectActionSummary', comment);
// If the current user does not exist, trigger sign in dialog.
if (!me) {
showSignInDialog();
return;
}
// If the current user is banned, do nothing.
if (me.status === 'BANNED') {
return;
}
if (myRespectActionSummary) {
deleteAction(myRespectActionSummary.current_user.id, comment.id);
} else {
postRespect({
item_id: comment.id,
item_type: 'COMMENTS'
});
}
}
render() {
const {comment} = this.props;
if (!comment) {
return null;
}
const myRespect = getMyActionSummary('RespectActionSummary', comment);
let count = getTotalActionCount('RespectActionSummary', comment);
return (
<div className={cn(styles.respect, `${name}-container`)}>
<button
className={cn(styles.button, {[styles.respected]: myRespect}, `${name}-button`)}
onClick={this.handleClick} >
<span className={`${name}-button-text`}>{t(myRespect ? 'respected' : 'respect')}</span>
<Icon className={cn(styles.icon, {[styles.respected]: myRespect}, `${name}-icon`)} />
<span className={`${name}-count`}>{count > 0 && count}</span>
</button>
</div>
);
}
}
RespectButton.propTypes = {
data: React.PropTypes.object.isRequired
};
export default RespectButton;
@@ -1,163 +0,0 @@
import {compose, gql} from 'react-apollo';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import get from 'lodash/get';
import {withFragments, withMutation} from 'coral-framework/hocs';
import {showSignInDialog} from 'coral-framework/actions/auth';
import RespectButton from '../components/RespectButton';
const isRespectAction = (a) => a.__typename === 'RespectActionSummary';
const COMMENT_FRAGMENT = gql`
fragment CoralRespect_UpdateFragment on Comment {
action_summaries {
... on RespectActionSummary {
count
current_user {
id
}
}
}
}
`;
const withDeleteAction = withMutation(gql`
mutation CoralRespect_DeleteAction($id: ID!) {
deleteAction(id:$id) {
errors {
translation_key
}
}
}
`, {
props: ({mutate}) => ({
deleteAction: (id, commentId) => {
return mutate({
variables: {id},
optimisticResponse: {
deleteAction: {
__typename: 'DeleteActionResponse',
errors: null,
}
},
update: (proxy) => {
const fragmentId = `Comment_${commentId}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
// Check whether we respected this comment.
const idx = data.action_summaries.findIndex(isRespectAction);
if (idx < 0 || get(data.action_summaries[idx], 'current_user.id') !== id) {
return;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count - 1,
current_user: null,
};
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
},
});
},
}),
});
const withPostRespect = withMutation(gql`
mutation CoralRespect_CreateRespect($respect: CreateRespectInput!) {
createRespect(respect: $respect) {
respect {
id
}
errors {
translation_key
}
}
}
`, {
props: ({mutate}) => ({
postRespect: (respect) => {
return mutate({
variables: {respect},
optimisticResponse: {
createRespect: {
__typename: 'CreateRespectResponse',
errors: null,
respect: {
__typename: 'RespectAction',
id: 'pending',
},
}
},
update: (proxy, mutationResult) => {
const fragmentId = `Comment_${respect.item_id}`;
// Read the data from our cache for this query.
const data = proxy.readFragment({fragment: COMMENT_FRAGMENT, id: fragmentId});
// Add our comment from the mutation to the end.
let idx = data.action_summaries.findIndex(isRespectAction);
// Check whether we already respected this comment.
if (idx >= 0 && data.action_summaries[idx].current_user) {
return;
}
if (idx < 0) {
// Add initial action when it doesn't exist.
data.action_summaries.push({
__typename: 'RespectActionSummary',
count: 0,
current_user: null,
});
idx = data.action_summaries.length - 1;
}
data.action_summaries[idx] = {
...data.action_summaries[idx],
count: data.action_summaries[idx].count + 1,
current_user: mutationResult.data.createRespect.respect,
};
// Write our data back to the cache.
proxy.writeFragment({fragment: COMMENT_FRAGMENT, id: fragmentId, data});
},
});
},
}),
});
const mapDispatchToProps = (dispatch) =>
bindActionCreators({showSignInDialog}, dispatch);
const enhance = compose(
withFragments({
root: gql`
fragment CoralRespect_RespectButton_root on RootQuery {
me {
status
}
}
`,
comment: gql`
fragment CoralRespect_RespectButton_comment on Comment {
action_summaries {
... on RespectActionSummary {
count
current_user {
id
}
}
}
}`,
}),
connect(null, mapDispatchToProps),
withDeleteAction,
withPostRespect,
);
export default enhance(RespectButton);
+3 -3
View File
@@ -1,9 +1,9 @@
import RespectButton from './containers/RespectButton';
import translations from './translations.json';
import RespectButton from './RespectButton';
import translations from './translations.yml';
export default {
translations,
slots: {
commentActions: [RespectButton],
commentReactions: [RespectButton]
}
};
@@ -1,6 +1,6 @@
.respect {
.container {
display: inline-block;
}
}
.button {
color: #2a2a2a;
@@ -1,10 +0,0 @@
{
"en": {
"respect": "Respect",
"respected": "Respected"
},
"es": {
"respect": "Respetar",
"respected": "Respetado"
}
}
@@ -0,0 +1,9 @@
en:
coral-plugin-respect:
respect: Respect
respected: Respected
es:
coral-plugin-respect:
respect: Respetar
respected: Respetado
+2 -41
View File
@@ -1,41 +1,2 @@
const {readFileSync} = require('fs');
const path = require('path');
const wrapResponse = require('../../graph/helpers/response');
module.exports = {
typeDefs: readFileSync(
path.join(__dirname, 'server/typeDefs.graphql'),
'utf8'
),
resolvers: {
RootMutation: {
createRespect(_, {respect: {item_id, item_type}}, {mutators: {Action}}) {
return wrapResponse('respect')(
Action.create({item_id, item_type, action_type: 'RESPECT'})
);
}
}
},
hooks: {
Action: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'RESPECT':
return 'RespectAction';
}
}
}
},
ActionSummary: {
__resolveType: {
post({action_type}) {
switch (action_type) {
case 'RESPECT':
return 'RespectActionSummary';
}
}
}
}
}
};
const {getReactionConfig} = require('../../plugin-api/beta/server');
module.exports = getReactionConfig('respect');
@@ -1,54 +0,0 @@
enum ACTION_TYPE {
# Represents a Respect.
RESPECT
}
input CreateRespectInput {
# The item's id for which we are to create a respect.
item_id: ID!
# The type of the item for which we are to create the respect.
item_type: ACTION_ITEM_TYPE!
}
# RespectAction is used by users who "respect" a specific entity.
type RespectAction implements Action {
# The ID of the action.
id: ID!
# The author of the action.
user: User
# The time when the Action was updated.
updated_at: Date
# The time when the Action was created.
created_at: Date
}
type RespectActionSummary implements ActionSummary {
# The count of actions with this group.
count: Int
# The current user's action.
current_user: RespectAction
}
type CreateRespectResponse implements Response {
# The respect that was created.
respect: RespectAction
# An array of errors relating to the mutation that occurred.
errors: [UserError]
}
type RootMutation {
# Creates a respect on an entity.
createRespect(respect: CreateRespectInput!): CreateRespectResponse
}
@@ -1,7 +1,8 @@
import React from 'react';
import cn from 'classnames';
import styles from './ViewingOptions.css';
import {Slot, Icon} from 'plugin-api/beta/client/components';
import {Slot} from 'plugin-api/beta/client/components';
import {Icon} from 'plugin-api/beta/client/components/ui';
const ViewingOptions = (props) => {
const toggleOpen = () => {
@@ -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,107 @@
import React from 'react';
import cn from 'classnames';
import styles from './styles.css';
import {t} from 'plugin-api/beta/client/services';
import {ClickOutside} from 'plugin-api/beta/client/components';
import {Icon, Button} from 'plugin-api/beta/client/components/ui';
const name = 'talk-plugin-permalink';
export default class PermalinkButton extends React.Component {
constructor (props) {
super(props);
this.state = {
popoverOpen: false,
copySuccessful: null,
copyFailure: null
};
}
toggle = () => {
this.popover.style.top = `${this.linkButton.offsetTop - 80}px`;
this.setState({
popoverOpen: !this.state.popoverOpen
});
}
handleClickOutside = () => {
this.setState({
popoverOpen: false
});
}
copyPermalink = () => {
this.permalinkInput.select();
try {
document.execCommand('copy');
this.setState({
copySuccessful: true,
copyFailure: null
});
} catch (err) {
this.setState({
copyFailure: true,
copySuccessful: null
});
}
this.timeout = window.setTimeout(() => {
this.setState({
copyFailure: null,
copySuccessful: null
});
}, 3000);
}
componentWillUnmount() {
window.clearTimeout(this.timeout);
}
render () {
const {copySuccessful, copyFailure, popoverOpen} = this.state;
const {asset} = this.props;
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={cn(`${name}-container`, styles.container)}>
<button
ref={(ref) => this.linkButton = ref}
onClick={this.toggle}
className={`${name}-button`}>
{t('permalink')}
<Icon name="link" />
</button>
<div
ref={(ref) => this.popover = ref}
className={cn([`${name}-popover`, styles.popover, {[styles.active]: popoverOpen}])}>
<input
className={cn(styles.input, `${name}-copy-field`)}
type='text'
ref={(input) => this.permalinkInput = input}
defaultValue={`${asset.url}?commentId=${this.props.commentId}`}
/>
<Button
onClick={this.copyPermalink}
className={cn([
styles.button,
`${name}-copy-button`, {
[styles.success]:copySuccessful,
[styles.failure]: copyFailure
}])}>
{!copyFailure && !copySuccessful && 'Copy'}
{copySuccessful && 'Copied'}
{copyFailure && 'Not supported'}
</Button>
</div>
</div>
</ClickOutside>
);
}
}
@@ -0,0 +1,77 @@
.popover {
display: none;
background-color: white;
position: absolute;
border-radius: 3px;
padding: 15px 10px;
box-sizing: border-box;
border: solid 1px rgba(153, 153, 153, 0.33);
left: 0;
width: 100%;
z-index: 10;
}
.popover::before {
content: '';
border: 10px solid transparent;
border-top-color: white;
position: absolute;
right: 7em;
bottom: -20px;
z-index: 2;
}
.popover::after{
content: '';
border: 10px solid transparent;
border-top-color: rgba(153, 153, 153, 0.33);
position: absolute;
right: 7em;
bottom: -21px;
z-index: 1;
}
.input {
display: inline-block;
width: calc(100% - 78px);
padding: 8px;
border-radius: 3px;
border: solid 1px #e0e0e0;
height: 32px;
box-sizing: border-box;
font-size: 1em;
}
.button {
display: inline-block;
float: right;
box-sizing: border-box;
margin: 0;
background-color: #e0e0e0;
font-size: 1em;
width: auto;
height: auto;
padding: 2px;
transition: background-color 0.4s ease;
}
.button:hover{
color: black;
}
.button.success {
background-color: #00897B;
color: white;
}
.button.failure {
background-color: #FF5252;
color: white;
}
.container {
z-index: 2;
}
.active {
display: block;
}
@@ -0,0 +1,7 @@
import PermalinkButton from './components/PermalinkButton';
export default {
slots: {
commentActions: [PermalinkButton]
}
};
+1
View File
@@ -0,0 +1 @@
module.exports = {};
@@ -0,0 +1,8 @@
{
"name": "talk-plugin-permalink",
"version": "1.0.0",
"description": "",
"main": "index.js",
"author": "",
"license": "ISC"
}
+4
View File
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1