mirror of
https://github.com/wassname/talk.git
synced 2026-07-13 17:45:56 +08:00
Merge branch 'master' into coral-rte
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Menu from './Menu';
|
||||
import styles from './AuthorName.css';
|
||||
import { ClickOutside } from 'plugin-api/beta/client/components';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default ({
|
||||
data,
|
||||
root,
|
||||
asset,
|
||||
comment,
|
||||
const AuthorName = ({
|
||||
slotPassthrough,
|
||||
username,
|
||||
contentSlot,
|
||||
menuVisible,
|
||||
toggleMenu,
|
||||
@@ -21,18 +20,23 @@ export default ({
|
||||
className={cn(styles.button, 'talk-plugin-author-menu-button')}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
<span className={styles.name}>{comment.user.username}</span>
|
||||
<span className={styles.name}>{username}</span>
|
||||
</button>
|
||||
{menuVisible && (
|
||||
<Menu
|
||||
data={data}
|
||||
root={root}
|
||||
asset={asset}
|
||||
comment={comment}
|
||||
contentSlot={contentSlot}
|
||||
/>
|
||||
<Menu slotPassthrough={slotPassthrough} contentSlot={contentSlot} />
|
||||
)}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
};
|
||||
|
||||
AuthorName.propTypes = {
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
menuVisible: PropTypes.bool.isRequired,
|
||||
toggleMenu: PropTypes.func.isRequired,
|
||||
hideMenu: PropTypes.func.isRequired,
|
||||
contentSlot: PropTypes.string,
|
||||
};
|
||||
|
||||
export default AuthorName;
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Menu.css';
|
||||
import { Slot } from 'plugin-api/beta/client/components';
|
||||
import cn from 'classnames';
|
||||
|
||||
export default ({ data, root, asset, comment, contentSlot }) => {
|
||||
const Menu = ({ slotPassthrough, contentSlot }) => {
|
||||
if (contentSlot) {
|
||||
return (
|
||||
<div className={cn(styles.menu, 'talk-plugin-author-menu-popup')}>
|
||||
<Slot
|
||||
fill={contentSlot}
|
||||
data={data}
|
||||
queryData={{ asset, root, comment }}
|
||||
/>
|
||||
<Slot fill={contentSlot} passthrough={slotPassthrough} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -21,15 +18,20 @@ export default ({ data, root, asset, comment, contentSlot }) => {
|
||||
<Slot
|
||||
className={cn('talk-plugin-author-menu-infos')}
|
||||
fill={'authorMenuInfos'}
|
||||
data={data}
|
||||
queryData={{ asset, root, comment }}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<Slot
|
||||
className={cn(styles.actions, 'talk-plugin-author-menu-actions')}
|
||||
fill={'authorMenuActions'}
|
||||
data={data}
|
||||
queryData={{ asset, root, comment }}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Menu.propTypes = {
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
contentSlot: PropTypes.string,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect, withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import AuthorName from '../components/AuthorName';
|
||||
@@ -47,21 +48,39 @@ class AuthorNameContainer extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
asset,
|
||||
comment,
|
||||
contentSlot,
|
||||
showMenuForComment,
|
||||
} = this.props;
|
||||
|
||||
const slotPassthrough = { root, asset, comment };
|
||||
|
||||
return (
|
||||
<AuthorName
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
comment={this.props.comment}
|
||||
contentSlot={this.props.contentSlot}
|
||||
menuVisible={this.props.showMenuForComment === this.props.comment.id}
|
||||
username={comment.user.username}
|
||||
contentSlot={contentSlot}
|
||||
menuVisible={showMenuForComment === comment.id}
|
||||
toggleMenu={this.toggleMenu}
|
||||
hideMenu={this.hideMenu}
|
||||
slotPassthrough={slotPassthrough}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorNameContainer.propTypes = {
|
||||
root: PropTypes.object.isRequired,
|
||||
asset: PropTypes.object.isRequired,
|
||||
comment: PropTypes.object.isRequired,
|
||||
contentSlot: PropTypes.string,
|
||||
showMenuForComment: PropTypes.string,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const slots = ['authorMenuInfos', 'authorMenuActions'];
|
||||
|
||||
const mapStateToProps = ({ talkPluginAuthorMenu: state }) => ({
|
||||
|
||||
@@ -18,8 +18,8 @@ class Comment extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { comment, asset, root, data } = this.props;
|
||||
const queryData = { comment, asset, root };
|
||||
const { comment, asset, root } = this.props;
|
||||
const slotPassthrough = { comment, asset, root };
|
||||
return (
|
||||
<div className={cn(styles.root, `${pluginName}-comment`)}>
|
||||
<Slot
|
||||
@@ -27,8 +27,7 @@ class Comment extends React.Component {
|
||||
className={cn(styles.quote, `${pluginName}-comment-body`)}
|
||||
fill="commentContent"
|
||||
defaultComponent={CommentContent}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
|
||||
<div className={cn(`${pluginName}-comment-username-box`)}>
|
||||
@@ -36,8 +35,7 @@ class Comment extends React.Component {
|
||||
className={cn(styles.username, `${pluginName}-comment-username`)}
|
||||
fill="commentAuthorName"
|
||||
defaultComponent={CommentAuthorName}
|
||||
queryData={queryData}
|
||||
data={data}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
@@ -45,9 +43,7 @@ class Comment extends React.Component {
|
||||
fill="commentTimestamp"
|
||||
defaultComponent={CommentTimestamp}
|
||||
className={cn(styles.timestamp, `${pluginName}-comment-timestamp`)}
|
||||
created_at={comment.created_at}
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={{ created_at: comment.created_at, ...slotPassthrough }}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
@@ -62,19 +58,11 @@ class Comment extends React.Component {
|
||||
>
|
||||
<Slot
|
||||
fill="commentReactions"
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
passthrough={slotPassthrough}
|
||||
inline
|
||||
/>
|
||||
|
||||
<FeaturedButton
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
/>
|
||||
<FeaturedButton root={root} comment={comment} asset={asset} />
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -22,7 +22,6 @@ class TabPane extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
root,
|
||||
data,
|
||||
asset: { featuredComments, ...asset },
|
||||
viewComment,
|
||||
} = this.props;
|
||||
@@ -32,7 +31,6 @@ class TabPane extends React.Component {
|
||||
<Comment
|
||||
key={comment.id}
|
||||
root={root}
|
||||
data={data}
|
||||
comment={comment}
|
||||
asset={asset}
|
||||
viewComment={viewComment}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { gql } from 'react-apollo';
|
||||
import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
|
||||
import { compose, withSubscribeToMore } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class ModIndicatorSubscription extends React.Component {
|
||||
subscriptions = null;
|
||||
@@ -27,7 +28,7 @@ class ModIndicatorSubscription extends React.Component {
|
||||
},
|
||||
];
|
||||
this.subscriptions = configs.map(config =>
|
||||
this.props.data.subscribeToMore(config)
|
||||
this.props.subscribeToMore(config)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,4 +61,4 @@ const COMMENT_UNFEATURED_SUBSCRIPTION = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
export default ModIndicatorSubscription;
|
||||
export default compose(withSubscribeToMore)(ModIndicatorSubscription);
|
||||
|
||||
@@ -6,6 +6,11 @@ import { getDefinitionName } from 'coral-framework/utils';
|
||||
import truncate from 'lodash/truncate';
|
||||
import t from 'coral-framework/services/i18n';
|
||||
import { subscriptionFields } from 'coral-admin/src/routes/Moderation/graphql';
|
||||
import {
|
||||
compose,
|
||||
withSubscribeToMore,
|
||||
withVariables,
|
||||
} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
function prepareNotificationText(text) {
|
||||
return truncate(text, { length: 50 }).replace('\n', ' ');
|
||||
@@ -19,7 +24,7 @@ class ModSubscription extends React.Component {
|
||||
{
|
||||
document: COMMENT_FEATURED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.data.variables.asset_id,
|
||||
assetId: this.props.variables.asset_id,
|
||||
},
|
||||
updateQuery: (
|
||||
prev,
|
||||
@@ -39,7 +44,7 @@ class ModSubscription extends React.Component {
|
||||
{
|
||||
document: COMMENT_UNFEATURED_SUBSCRIPTION,
|
||||
variables: {
|
||||
assetId: this.props.data.variables.asset_id,
|
||||
assetId: this.props.variables.asset_id,
|
||||
},
|
||||
updateQuery: (
|
||||
prev,
|
||||
@@ -62,7 +67,7 @@ class ModSubscription extends React.Component {
|
||||
},
|
||||
];
|
||||
this.subscriptions = configs.map(config =>
|
||||
this.props.data.subscribeToMore(config)
|
||||
this.props.subscribeToMore(config)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,4 +116,8 @@ const mapStateToProps = state => ({
|
||||
user: state.auth.user,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(ModSubscription);
|
||||
export default compose(
|
||||
connect(mapStateToProps, null),
|
||||
withVariables,
|
||||
withSubscribeToMore
|
||||
)(ModSubscription);
|
||||
|
||||
@@ -2,7 +2,12 @@ import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import TabPane from '../components/TabPane';
|
||||
import { withFragments, connect } from 'plugin-api/beta/client/hocs';
|
||||
import {
|
||||
withFragments,
|
||||
connect,
|
||||
withFetchMore,
|
||||
withVariables,
|
||||
} from 'plugin-api/beta/client/hocs';
|
||||
import Comment from '../containers/Comment';
|
||||
import { viewComment } from 'coral-embed-stream/src/actions/stream';
|
||||
import {
|
||||
@@ -13,15 +18,15 @@ import update from 'immutability-helper';
|
||||
|
||||
class TabPaneContainer extends React.Component {
|
||||
loadMore = () => {
|
||||
return this.props.data.fetchMore({
|
||||
return this.props.fetchMore({
|
||||
query: LOAD_MORE_QUERY,
|
||||
variables: {
|
||||
limit: 5,
|
||||
cursor: this.props.asset.featuredComments.endCursor,
|
||||
asset_id: this.props.asset.id,
|
||||
sortOrder: this.props.data.variables.sortOrder,
|
||||
sortBy: this.props.data.variables.sortBy,
|
||||
excludeIgnored: this.props.data.variables.excludeIgnored,
|
||||
sortOrder: this.props.variables.sortOrder,
|
||||
sortBy: this.props.variables.sortBy,
|
||||
excludeIgnored: this.props.variables.excludeIgnored,
|
||||
},
|
||||
updateQuery: (previous, { fetchMoreResult: { comments } }) => {
|
||||
const updated = update(previous, {
|
||||
@@ -86,6 +91,8 @@ const mapDispatchToProps = dispatch =>
|
||||
|
||||
const enhance = compose(
|
||||
connect(null, mapDispatchToProps),
|
||||
withFetchMore,
|
||||
withVariables,
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkFeaturedComments_TabPane_root on RootQuery {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
|
||||
class FlagDetails extends Component {
|
||||
render() {
|
||||
const { comment: { actions }, more, data, root, comment } = this.props;
|
||||
const { comment: { actions }, more, root, comment } = this.props;
|
||||
|
||||
const flagActions =
|
||||
actions && actions.filter(a => a.__typename === 'FlagAction');
|
||||
@@ -26,7 +26,7 @@ class FlagDetails extends Component {
|
||||
}, {});
|
||||
|
||||
const reasons = Object.keys(summaries);
|
||||
const queryData = {
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
comment,
|
||||
};
|
||||
@@ -52,12 +52,11 @@ class FlagDetails extends Component {
|
||||
{more && (
|
||||
<IfSlotIsNotEmpty
|
||||
slot="adminCommentMoreFlagDetails"
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<Slot
|
||||
fill="adminCommentMoreFlagDetails"
|
||||
data={data}
|
||||
queryData={queryData}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</IfSlotIsNotEmpty>
|
||||
)}
|
||||
@@ -68,7 +67,6 @@ class FlagDetails extends Component {
|
||||
|
||||
FlagDetails.propTypes = {
|
||||
more: PropTypes.bool,
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
comment: PropTypes.shape({
|
||||
actions: PropTypes.arrayOf(
|
||||
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
createComment: {
|
||||
comment: {
|
||||
user: {
|
||||
created_at: new Date(),
|
||||
created_at: new Date().toISOString(),
|
||||
__typename: 'User',
|
||||
},
|
||||
__typename: 'Comment',
|
||||
|
||||
@@ -16,12 +16,16 @@ export default class ModerationActions extends React.Component {
|
||||
comment,
|
||||
root,
|
||||
asset,
|
||||
data,
|
||||
menuVisible,
|
||||
toogleMenu,
|
||||
hideMenu,
|
||||
} = this.props;
|
||||
|
||||
const slotPassthrough = {
|
||||
comment,
|
||||
asset,
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickOutside onClickOutside={hideMenu}>
|
||||
<div
|
||||
@@ -45,8 +49,7 @@ export default class ModerationActions extends React.Component {
|
||||
<Slot
|
||||
className="talk-plugin-modetarion-actions-slot"
|
||||
fill="moderationActions"
|
||||
queryData={{ comment, asset }}
|
||||
data={data}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
<ApproveCommentAction comment={comment} hideMenu={hideMenu} />
|
||||
<RejectCommentAction comment={comment} hideMenu={hideMenu} />
|
||||
@@ -67,7 +70,6 @@ ModerationActions.propTypes = {
|
||||
comment: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
asset: PropTypes.object,
|
||||
data: PropTypes.object,
|
||||
menuVisible: PropTypes.bool,
|
||||
toogleMenu: PropTypes.func,
|
||||
hideMenu: PropTypes.func,
|
||||
|
||||
@@ -42,7 +42,6 @@ class ModerationActionsContainer extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<ModerationActions
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
asset={this.props.asset}
|
||||
comment={this.props.comment}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Toggle from 'talk-plugin-notifications/client/components/Toggle';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class ToggleContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.setTurnOffInputFragment({ onFeatured: false });
|
||||
|
||||
if (this.getOnFeaturedSetting()) {
|
||||
props.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const prevSetting = this.getOnFeaturedSetting(this.props);
|
||||
const nextSetting = this.getOnFeaturedSetting(nextProps);
|
||||
if (prevSetting && !nextSetting) {
|
||||
nextProps.indicateOff();
|
||||
} else if (!prevSetting && nextSetting) {
|
||||
nextProps.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
getOnFeaturedSetting = (props = this.props) =>
|
||||
props.root.me.notificationSettings.onFeatured;
|
||||
|
||||
toggle = () => {
|
||||
this.props.updateNotificationSettings({
|
||||
onFeatured: !this.getOnFeaturedSetting(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toggle
|
||||
checked={this.getOnFeaturedSetting()}
|
||||
onChange={this.toggle}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{t('talk-plugin-notifications-category-featured.toggle_description')}
|
||||
</Toggle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
indicateOn: PropTypes.func.isRequired,
|
||||
indicateOff: PropTypes.func.isRequired,
|
||||
setTurnOffInputFragment: PropTypes.func.isRequired,
|
||||
updateNotificationSettings: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkNotificationsCategoryFeatured_Toggle_root on RootQuery {
|
||||
me {
|
||||
notificationSettings {
|
||||
onFeatured
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(ToggleContainer);
|
||||
@@ -1,33 +0,0 @@
|
||||
import { gql } from 'react-apollo';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
UpdateNotificationSettings: ({
|
||||
variables: { input },
|
||||
state: { auth: { user: { id } } },
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
if (input.onFeatured === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = gql`
|
||||
fragment TalkNotificationsCategoryFeatured_User_Fragment on User {
|
||||
notificationSettings {
|
||||
onFeatured
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
notificationSettings: {
|
||||
__typename: 'NotificationSettings',
|
||||
onFeatured: input.onFeatured,
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({ fragment, id: fragmentId, data });
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import Toggle from './containers/Toggle';
|
||||
import translations from './translations.yml';
|
||||
import graphql from './graphql';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories';
|
||||
|
||||
const SettingsToggle = createSettingsToggle('onFeatured', () =>
|
||||
t('talk-plugin-notifications-category-featured.toggle_description')
|
||||
);
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
notificationSettings: [Toggle],
|
||||
notificationSettings: [SettingsToggle],
|
||||
},
|
||||
translations,
|
||||
...graphql,
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Toggle from 'talk-plugin-notifications/client/components/Toggle';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class ToggleContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.setTurnOffInputFragment({ onReply: false });
|
||||
|
||||
if (this.getOnReplySetting()) {
|
||||
props.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const prevSetting = this.getOnReplySetting(this.props);
|
||||
const nextSetting = this.getOnReplySetting(nextProps);
|
||||
if (prevSetting && !nextSetting) {
|
||||
nextProps.indicateOff();
|
||||
} else if (!prevSetting && nextSetting) {
|
||||
nextProps.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
getOnReplySetting = (props = this.props) =>
|
||||
props.root.me.notificationSettings.onReply;
|
||||
|
||||
toggle = () => {
|
||||
this.props.updateNotificationSettings({
|
||||
onReply: !this.getOnReplySetting(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toggle
|
||||
checked={this.getOnReplySetting()}
|
||||
onChange={this.toggle}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{t('talk-plugin-notifications-category-reply.toggle_description')}
|
||||
</Toggle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
indicateOn: PropTypes.func.isRequired,
|
||||
indicateOff: PropTypes.func.isRequired,
|
||||
setTurnOffInputFragment: PropTypes.func.isRequired,
|
||||
updateNotificationSettings: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkNotificationsCategoryReply_Toggle_root on RootQuery {
|
||||
me {
|
||||
notificationSettings {
|
||||
onReply
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(ToggleContainer);
|
||||
@@ -1,33 +0,0 @@
|
||||
import { gql } from 'react-apollo';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
UpdateNotificationSettings: ({
|
||||
variables: { input },
|
||||
state: { auth: { user: { id } } },
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
if (input.onReply === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = gql`
|
||||
fragment TalkNotificationsCategoryReply_User_Fragment on User {
|
||||
notificationSettings {
|
||||
onReply
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
notificationSettings: {
|
||||
__typename: 'NotificationSettings',
|
||||
onReply: input.onReply,
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({ fragment, id: fragmentId, data });
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import Toggle from './containers/Toggle';
|
||||
import translations from './translations.yml';
|
||||
import graphql from './graphql';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories';
|
||||
|
||||
const SettingsToggle = createSettingsToggle('onReply', () =>
|
||||
t('talk-plugin-notifications-category-reply.toggle_description')
|
||||
);
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
notificationSettings: [Toggle],
|
||||
notificationSettings: [SettingsToggle],
|
||||
},
|
||||
translations,
|
||||
...graphql,
|
||||
};
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import Toggle from 'talk-plugin-notifications/client/components/Toggle';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
|
||||
class ToggleContainer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.setTurnOffInputFragment({ onStaffReply: false });
|
||||
|
||||
if (this.getOnReplySetting()) {
|
||||
props.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const prevSetting = this.getOnReplySetting(this.props);
|
||||
const nextSetting = this.getOnReplySetting(nextProps);
|
||||
if (prevSetting && !nextSetting) {
|
||||
nextProps.indicateOff();
|
||||
} else if (!prevSetting && nextSetting) {
|
||||
nextProps.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
getOnReplySetting = (props = this.props) =>
|
||||
props.root.me.notificationSettings.onStaffReply;
|
||||
|
||||
toggle = () => {
|
||||
this.props.updateNotificationSettings({
|
||||
onStaffReply: !this.getOnReplySetting(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toggle
|
||||
checked={this.getOnReplySetting()}
|
||||
onChange={this.toggle}
|
||||
disabled={this.props.disabled}
|
||||
>
|
||||
{t('talk-plugin-notifications-category-staff.toggle_description')}
|
||||
</Toggle>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ToggleContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
indicateOn: PropTypes.func.isRequired,
|
||||
indicateOff: PropTypes.func.isRequired,
|
||||
setTurnOffInputFragment: PropTypes.func.isRequired,
|
||||
updateNotificationSettings: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkNotificationsCategoryStaffReply_User_Fragment on RootQuery {
|
||||
me {
|
||||
notificationSettings {
|
||||
onStaffReply
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(ToggleContainer);
|
||||
@@ -1,33 +0,0 @@
|
||||
import { gql } from 'react-apollo';
|
||||
|
||||
export default {
|
||||
mutations: {
|
||||
UpdateNotificationSettings: ({
|
||||
variables: { input },
|
||||
state: { auth: { user: { id } } },
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
if (input.onStaffReply === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = gql`
|
||||
fragment TalkNotificationsCategoryStaffReply_User_Fragment on User {
|
||||
notificationSettings {
|
||||
onStaffReply
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
notificationSettings: {
|
||||
__typename: 'NotificationSettings',
|
||||
onStaffReply: input.onStaffReply,
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({ fragment, id: fragmentId, data });
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
import Toggle from './containers/Toggle';
|
||||
import translations from './translations.yml';
|
||||
import graphql from './graphql';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
import { createSettingsToggle } from 'talk-plugin-notifications/client/api/factories';
|
||||
|
||||
const SettingsToggle = createSettingsToggle('onStaffReply', () =>
|
||||
t('talk-plugin-notifications-category-staff.toggle_description')
|
||||
);
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
notificationSettings: [Toggle],
|
||||
notificationSettings: [SettingsToggle],
|
||||
},
|
||||
translations,
|
||||
...graphql,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.title {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&.disabled {
|
||||
color: #e5e5e5;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkBox {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Checkbox } from 'plugin-api/beta/client/components/ui';
|
||||
import styles from './Toggle.css';
|
||||
import uuid from 'uuid/v4';
|
||||
import cn from 'classnames';
|
||||
|
||||
class Toggle extends React.Component {
|
||||
id = uuid();
|
||||
|
||||
render() {
|
||||
const { checked, onChange, children, disabled } = this.props;
|
||||
return (
|
||||
<div className={styles.toggle}>
|
||||
<label
|
||||
htmlFor={this.id}
|
||||
className={cn(styles.title, { [styles.disabled]: disabled })}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
<div className={styles.checkBox}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
id={this.id}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Toggle.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
checked: PropTypes.bool,
|
||||
onChange: PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default Toggle;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toggle } from './Toggle';
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import Toggle from '../components/Toggle';
|
||||
import withSettingsToggle from '../hocs/withSettingsToggle';
|
||||
|
||||
/**
|
||||
* createSettingsToggle will add a boolean setting with the
|
||||
* name `settingsName` to notification settings and return
|
||||
* a full Toggle Component.
|
||||
*
|
||||
* You must provide a `label` either as a string or as a callback.
|
||||
* E.g. to provide translations you could do:
|
||||
*
|
||||
* `const SettingsToggle = createSettingsToggle('onReply', () => t('translate'));`
|
||||
*/
|
||||
const createSettingsToggle = (settingsName, label) => {
|
||||
const SettingsToggle = props => (
|
||||
<Toggle {...props}>{typeof label === 'function' ? label() : label}</Toggle>
|
||||
);
|
||||
return withSettingsToggle(settingsName)(SettingsToggle);
|
||||
};
|
||||
|
||||
export default createSettingsToggle;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as createSettingsToggle } from './createSettingsToggle';
|
||||
@@ -0,0 +1 @@
|
||||
export { default as withSettingsToggle } from './withSettingsToggle';
|
||||
@@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import {
|
||||
withFragments,
|
||||
withGraphQLExtension,
|
||||
} from 'plugin-api/beta/client/hocs';
|
||||
|
||||
const createHOC = settingsName => WrappedComponent => {
|
||||
class WithSettingsToggle extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
props.setTurnOffInputFragment({ [settingsName]: false });
|
||||
|
||||
if (this.isChecked()) {
|
||||
props.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const prevSetting = this.isChecked(this.props);
|
||||
const nextSetting = this.isChecked(nextProps);
|
||||
if (prevSetting && !nextSetting) {
|
||||
nextProps.indicateOff();
|
||||
} else if (!prevSetting && nextSetting) {
|
||||
nextProps.indicateOn();
|
||||
}
|
||||
}
|
||||
|
||||
isChecked = (props = this.props) =>
|
||||
props.root.me.notificationSettings[settingsName];
|
||||
|
||||
toggle = () => {
|
||||
this.props.updateNotificationSettings({
|
||||
[settingsName]: !this.isChecked(),
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
checked={this.isChecked()}
|
||||
onChange={this.toggle}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WithSettingsToggle.propTypes = {
|
||||
root: PropTypes.object,
|
||||
indicateOn: PropTypes.func.isRequired,
|
||||
indicateOff: PropTypes.func.isRequired,
|
||||
setTurnOffInputFragment: PropTypes.func.isRequired,
|
||||
updateNotificationSettings: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
return WithSettingsToggle;
|
||||
};
|
||||
|
||||
/**
|
||||
* withSettingsToggle will add a boolean setting with the
|
||||
* name `settingsName` to notification settings and provide
|
||||
* the folliwng props:
|
||||
*
|
||||
* `checked: boolean` Whether setting is on or off
|
||||
* `onChange: () => void` Calling this will toggle the setting
|
||||
* `disabled: boolean` Whether setting is disabled
|
||||
*/
|
||||
const withSettingsToggle = settingsName => {
|
||||
const extension = {
|
||||
mutations: {
|
||||
UpdateNotificationSettings: ({
|
||||
variables: { input },
|
||||
state: { auth: { user: { id } } },
|
||||
}) => ({
|
||||
update: proxy => {
|
||||
if (input[settingsName] === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fragment = gql`
|
||||
fragment TalkNotifications_Toggle_${settingsName}_Fragment on User {
|
||||
notificationSettings {
|
||||
${settingsName}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const fragmentId = `User_${id}`;
|
||||
const data = {
|
||||
__typename: 'User',
|
||||
notificationSettings: {
|
||||
__typename: 'NotificationSettings',
|
||||
[settingsName]: input[settingsName],
|
||||
},
|
||||
};
|
||||
proxy.writeFragment({ fragment, id: fragmentId, data });
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkNotifications_Toggle_${settingsName}_root on RootQuery {
|
||||
me {
|
||||
notificationSettings {
|
||||
${settingsName}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
}),
|
||||
withGraphQLExtension(extension),
|
||||
createHOC(settingsName)
|
||||
);
|
||||
};
|
||||
|
||||
export default withSettingsToggle;
|
||||
@@ -37,15 +37,18 @@ class Settings extends React.Component {
|
||||
onChangeDigestFrequency,
|
||||
} = this.props;
|
||||
|
||||
const slotProps = {
|
||||
queryData: { root },
|
||||
setTurnOffInputFragment: setTurnOffInputFragment,
|
||||
updateNotificationSettings: updateNotificationSettings,
|
||||
const slotPassthrough = {
|
||||
root,
|
||||
setTurnOffInputFragment,
|
||||
updateNotificationSettings,
|
||||
disabled: needEmailVerification,
|
||||
};
|
||||
|
||||
return (
|
||||
<IfSlotIsNotEmpty slot="notificationSettings" {...slotProps}>
|
||||
<IfSlotIsNotEmpty
|
||||
slot="notificationSettings"
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<div className={styles.root}>
|
||||
<h3>{t('talk-plugin-notifications.settings_title')}</h3>
|
||||
<div className={styles.bannerContainer}>
|
||||
@@ -63,7 +66,7 @@ class Settings extends React.Component {
|
||||
className={styles.notifcationSettingsSlot}
|
||||
fill="notificationSettings"
|
||||
childFactory={this.childFactory}
|
||||
{...slotProps}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
{digestFrequencyValues.length > 1 && (
|
||||
|
||||
@@ -50,7 +50,6 @@ class SettingsContainer extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Settings
|
||||
data={this.props.data}
|
||||
root={this.props.root}
|
||||
indicateOn={this.indicateOn}
|
||||
indicateOff={this.indicateOff}
|
||||
@@ -72,7 +71,6 @@ class SettingsContainer extends React.Component {
|
||||
}
|
||||
|
||||
SettingsContainer.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
updateNotificationSettings: PropTypes.func.isRequired,
|
||||
digestFrequencyValues: PropTypes.array.isRequired,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from 'react';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
|
||||
const Tab = () => {
|
||||
return <span>{t('talk-plugin-profile-settings.tab')}</span>;
|
||||
};
|
||||
|
||||
export default Tab;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Slot } from 'plugin-api/beta/client/components';
|
||||
|
||||
class TabPane extends React.Component {
|
||||
render() {
|
||||
const { data, root } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Slot fill="profileSettings" data={data} queryData={{ root }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabPane.propTypes = {
|
||||
data: PropTypes.object,
|
||||
root: PropTypes.object,
|
||||
};
|
||||
|
||||
export default TabPane;
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import TabPane from '../components/TabPane';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
|
||||
|
||||
const slots = ['profileSettings'];
|
||||
|
||||
class TabPaneContainer extends React.Component {
|
||||
render() {
|
||||
return <TabPane {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
const enhance = compose(
|
||||
withFragments({
|
||||
root: gql`
|
||||
fragment TalkProfileSettings_TabPane_root on RootQuery {
|
||||
__typename
|
||||
${getSlotFragmentSpreads(slots, 'root')}
|
||||
}
|
||||
`,
|
||||
})
|
||||
);
|
||||
|
||||
export default enhance(TabPaneContainer);
|
||||
@@ -1,11 +0,0 @@
|
||||
import Tab from './components/Tab';
|
||||
import TabPane from './containers/TabPane';
|
||||
import translations from './translations.yml';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
profileTabs: [Tab],
|
||||
profileTabPanes: [TabPane],
|
||||
},
|
||||
translations,
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
ar:
|
||||
talk-plugin-profile-settings:
|
||||
tab: إعدادات
|
||||
en:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Settings
|
||||
de:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Einstellungen
|
||||
es:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Configuración
|
||||
fr:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Paramètres
|
||||
nl_NL:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Instellingen
|
||||
da:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Indstillinger
|
||||
pt_PR:
|
||||
talk-plugin-profile-settings:
|
||||
tab: Configurações
|
||||
zh_TW:
|
||||
talk-plugin-profile-settings:
|
||||
tab: 設置
|
||||
zh_CN:
|
||||
talk-plugin-profile-settings:
|
||||
tab: 设置
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = {};
|
||||
@@ -1,49 +0,0 @@
|
||||
---
|
||||
title: talk-plugin-rich-text-pell
|
||||
permalink: /plugin/talk-plugin-rich-text-pell/
|
||||
layout: plugin
|
||||
plugin:
|
||||
name: talk-plugin-rich-text-pell
|
||||
depends:
|
||||
- name: talk-plugin-rich-text
|
||||
provides:
|
||||
- Client
|
||||
---
|
||||
|
||||
Enables rich text support client-side by using [Pell](https://github.com/jaredreich/pell).
|
||||
|
||||
## Installation
|
||||
|
||||
Add `"talk-plugin-rich-text-pell"` to the `plugins.json` in your Talk
|
||||
installation. Remember to add this in the `client` property since this plugin
|
||||
only covers the client side. To add server support, please use
|
||||
[talk-plugin-rich-text](/talk/plugin/talk-plugin-rich-text).
|
||||
|
||||
_Note: Ensure that you don't have any other plugins utilizing the
|
||||
`commentContent` slot, as it would result in duplicate comments._
|
||||
|
||||
## How does this work?
|
||||
|
||||
This plugin contains 2 important components:
|
||||
|
||||
- The Editor (`./components/Editor.js`)
|
||||
- The Comment Content Renderer (`./components/CommentContent.js`)
|
||||
|
||||
The editor component contains the rich text editor. For this particular plugin
|
||||
we chose [Pell](https://github.com/jaredreich/pell). Pell is the simplest and
|
||||
smallest WYSIWYG text editor with no dependencies that we could find.
|
||||
|
||||
If you check our `index.js` you will notice that we inject this editor in the
|
||||
`commentBox` slot. We do this to replace the core comment box with this one.
|
||||
|
||||
Now, in order to render the new styled comments we need a comment renderer. For
|
||||
this task we will have to replace our core comment renderer by using the
|
||||
`commentContent` slot.
|
||||
|
||||
If you are not familiar with GraphQL `client/index.js` will look complicated,
|
||||
but fear not! With those functions we specify what to expect from the server
|
||||
schema, how to perform optimistic updates and how keep the client store updated
|
||||
with the latest changes.
|
||||
|
||||
We encourage you to see the files and check how easy is to build plugins! If you
|
||||
have any feedback, please let us know.
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "@coralproject/eslint-config-talk/client"
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { pluginName } from '../../package.json';
|
||||
|
||||
class CommentContent extends React.Component {
|
||||
render() {
|
||||
const { comment } = this.props;
|
||||
return comment.richTextBody ? (
|
||||
<div
|
||||
className={`${pluginName}-text`}
|
||||
dangerouslySetInnerHTML={{ __html: comment.richTextBody }}
|
||||
/>
|
||||
) : (
|
||||
<div className={`${pluginName}-text`}>{comment.body}</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentContent.propTypes = {
|
||||
comment: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default CommentContent;
|
||||
@@ -1,41 +0,0 @@
|
||||
.content {
|
||||
background: #fff;
|
||||
border: solid 1px #bbb;
|
||||
min-height: 120px;
|
||||
box-sizing: border-box;
|
||||
outline: 0;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-style: unset;
|
||||
}
|
||||
|
||||
.button > i {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: transparent;
|
||||
padding: 3px;
|
||||
border: none;
|
||||
color: #4e4e4e;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.button:hover{
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
background-color: #eae8e8;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
user-select: none;
|
||||
padding: 5px 10px;
|
||||
border-top: 1px solid #bbb;
|
||||
border-left: 1px solid #bbb;
|
||||
border-right: 1px solid #bbb;
|
||||
}
|
||||
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { init } from 'pell';
|
||||
import styles from './Editor.css';
|
||||
import cn from 'classnames';
|
||||
import { pluginName } from '../../package.json';
|
||||
import { htmlNormalizer } from '../utils';
|
||||
|
||||
class Editor extends React.Component {
|
||||
ref = null;
|
||||
|
||||
handleRef = ref => (this.ref = ref);
|
||||
|
||||
componentDidMount() {
|
||||
const { onChange, actions, classNames, isReply } = this.props;
|
||||
|
||||
init({
|
||||
element: this.ref,
|
||||
onChange: richTextBody => {
|
||||
// We want to save the original comment body
|
||||
const originalBody = this.ref.childNodes[1].innerText;
|
||||
onChange(originalBody, { richTextBody: htmlNormalizer(richTextBody) });
|
||||
},
|
||||
actions,
|
||||
classes: {
|
||||
actionbar: cn(
|
||||
styles.actionBar,
|
||||
classNames.actionbar,
|
||||
`${pluginName}-action-bar`
|
||||
),
|
||||
content: cn(
|
||||
styles.content,
|
||||
classNames.content,
|
||||
`${pluginName}-content`
|
||||
),
|
||||
button: cn(styles.button, classNames.button, `${pluginName}-button`),
|
||||
},
|
||||
});
|
||||
|
||||
// To edit comments and have the previous html comment
|
||||
if (this.props.comment && this.props.comment.richTextBody && !isReply) {
|
||||
this.ref.content.innerHTML = this.props.comment.richTextBody;
|
||||
}
|
||||
|
||||
if (this.props.registerHook) {
|
||||
this.clearInputHook = this.props.registerHook(
|
||||
'postSubmit',
|
||||
(res, handleBodyChange) => {
|
||||
this.ref.content.innerHTML = '';
|
||||
handleBodyChange('', { richTextBody: '' });
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.unregisterHook(this.clearInputHook);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { id, classNames } = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
ref={this.handleRef}
|
||||
className={cn(
|
||||
styles.container,
|
||||
classNames.container,
|
||||
`${pluginName}-container`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Editor.defaultProps = {
|
||||
defaultContent: '',
|
||||
styleWithCSS: false,
|
||||
actions: [
|
||||
{ name: 'bold', icon: '<i class="material-icons">format_bold</i>' },
|
||||
{ name: 'italic', icon: '<i class="material-icons">format_italic</i>' },
|
||||
{ name: 'quote', icon: '<i class="material-icons">format_quote</i>' },
|
||||
],
|
||||
classNames: {
|
||||
button: '',
|
||||
content: '',
|
||||
actionbar: '',
|
||||
container: '',
|
||||
},
|
||||
};
|
||||
|
||||
Editor.propTypes = {
|
||||
id: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
placeholder: PropTypes.string,
|
||||
onChange: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
rows: PropTypes.number,
|
||||
comment: PropTypes.object,
|
||||
classNames: PropTypes.object,
|
||||
actions: PropTypes.array,
|
||||
registerHook: PropTypes.func,
|
||||
unregisterHook: PropTypes.func,
|
||||
isReply: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import CommentContent from '../components/CommentContent';
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
fragment TalkPluginRTE_CommentContent_comment on Comment {
|
||||
body
|
||||
richTextBody
|
||||
}
|
||||
`,
|
||||
})(CommentContent);
|
||||
@@ -1,12 +0,0 @@
|
||||
import { gql } from 'react-apollo';
|
||||
import { withFragments } from 'plugin-api/beta/client/hocs';
|
||||
import Editor from '../components/Editor';
|
||||
|
||||
export default withFragments({
|
||||
comment: gql`
|
||||
fragment TalkPluginRTE_Editor_comment on Comment {
|
||||
body
|
||||
richTextBody
|
||||
}
|
||||
`,
|
||||
})(Editor);
|
||||
@@ -1,70 +0,0 @@
|
||||
import Editor from './containers/Editor';
|
||||
import CommentContent from './containers/CommentContent';
|
||||
import { gql } from 'react-apollo';
|
||||
|
||||
export default {
|
||||
slots: {
|
||||
draftArea: [Editor],
|
||||
commentContent: [CommentContent],
|
||||
adminCommentContent: [CommentContent],
|
||||
userDetailCommentContent: [CommentContent],
|
||||
},
|
||||
fragments: {
|
||||
CreateCommentResponse: gql`
|
||||
fragment TalkRTE_CreateCommentResponse on CreateCommentResponse {
|
||||
comment {
|
||||
richTextBody
|
||||
}
|
||||
}
|
||||
`,
|
||||
EditCommentResponse: gql`
|
||||
fragment TalkRTE_EditCommentResponse on EditCommentResponse {
|
||||
comment {
|
||||
richTextBody
|
||||
}
|
||||
}
|
||||
`,
|
||||
},
|
||||
mutations: {
|
||||
PostComment: ({ variables: { input } }) => {
|
||||
return {
|
||||
optimisticResponse: {
|
||||
createComment: {
|
||||
comment: {
|
||||
richTextBody: input.richTextBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
EditComment: ({ variables: { id, edit } }) => {
|
||||
return {
|
||||
optimisticResponse: {
|
||||
editComment: {
|
||||
comment: {
|
||||
richTextBody: edit.richTextBody,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: proxy => {
|
||||
const editCommentFragment = gql`
|
||||
fragment Talk_EditComment on Comment {
|
||||
richTextBody
|
||||
}
|
||||
`;
|
||||
|
||||
const fragmentId = `Comment_${id}`;
|
||||
|
||||
proxy.writeFragment({
|
||||
fragment: editCommentFragment,
|
||||
id: fragmentId,
|
||||
data: {
|
||||
__typename: 'Comment',
|
||||
richTextBody: edit.richTextBody,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
export function htmlNormalizer(htmlInput) {
|
||||
let str = htmlInput;
|
||||
// We are normalizing the input from contenteditable of each browser, also removing unnecesary html tags
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content#Differences_in_markup_generation
|
||||
|
||||
// Old browsers uses `p` normalize to `div` instead.
|
||||
str = str
|
||||
.replace(/<p>/g, '<div>') // IE and old browsers outputs <p> instead of <div>s
|
||||
.replace(/<\/p>/g, '</div>'); // IE and old browsers outputs <p> instead of <div>s
|
||||
|
||||
// Remove first opening tag, otherwise
|
||||
// with the following transformation below
|
||||
// we might add an unintended first empty line.
|
||||
if (str.startsWith('<div>')) {
|
||||
str = str.replace('<div>', '');
|
||||
}
|
||||
|
||||
// Normalize <div>s to <br>.
|
||||
return str.replace(/<div>/g, '<br>').replace(/<\/div>/g, '');
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = {};
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "@coralproject/talk-plugin-rich-text-pell",
|
||||
"pluginName": "talk-plugin-rich-text-pell",
|
||||
"version": "0.0.1",
|
||||
"description": "Pell's Rich Text Editor for Talk",
|
||||
"main": "index.js",
|
||||
"author": "The Coral Project Team <coral@mozillafoundation.org>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"pell": "^1.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './Category.css';
|
||||
import { Slot } from 'plugin-api/beta/client/components';
|
||||
|
||||
@@ -8,7 +9,7 @@ const childFactory = child => (
|
||||
</li>
|
||||
);
|
||||
|
||||
const ViewingOptions = ({ slot, title, data, asset, root }) => {
|
||||
const Category = ({ slot, title, slotPassthrough }) => {
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.title}>{title}</div>
|
||||
@@ -17,11 +18,16 @@ const ViewingOptions = ({ slot, title, data, asset, root }) => {
|
||||
childFactory={childFactory}
|
||||
className={styles.list}
|
||||
component={'ul'}
|
||||
data={data}
|
||||
queryData={{ asset, root }}
|
||||
passthrough={slotPassthrough}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewingOptions;
|
||||
Category.propTypes = {
|
||||
slot: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Category;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './Menu.css';
|
||||
import { capitalize } from 'plugin-api/beta/client/utils';
|
||||
@@ -13,16 +14,19 @@ class Menu extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { slotPassthrough } = this.props;
|
||||
return (
|
||||
<div className={cn([styles.menu, 'talk-plugin-viewing-options-menu'])}>
|
||||
{Object.keys(this.categories).map(category => (
|
||||
<IfSlotIsNotEmpty
|
||||
slot={`viewingOptions${capitalize(category)}`}
|
||||
key={category}
|
||||
passthrough={slotPassthrough}
|
||||
>
|
||||
<Category
|
||||
slot={`viewingOptions${capitalize(category)}`}
|
||||
title={this.categories[category]}
|
||||
slotPassthrough={slotPassthrough}
|
||||
/>
|
||||
</IfSlotIsNotEmpty>
|
||||
))}
|
||||
@@ -31,4 +35,8 @@ class Menu extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
Menu.propTypes = {
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Menu;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cn from 'classnames';
|
||||
import styles from './ViewingOptions.css';
|
||||
import { t } from 'plugin-api/beta/client/services';
|
||||
@@ -24,7 +25,7 @@ class ViewingOptions extends React.Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { open, data, root, asset } = this.props;
|
||||
const { open, slotPassthrough } = this.props;
|
||||
return (
|
||||
<ClickOutside onClickOutside={this.handleClickOutside}>
|
||||
<div className={cn([styles.root, 'talk-plugin-viewing-options'])}>
|
||||
@@ -41,11 +42,18 @@ class ViewingOptions extends React.Component {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{open && <Menu data={data} root={root} asset={asset} />}
|
||||
{open && <Menu slotPassthrough={slotPassthrough} />}
|
||||
</div>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ViewingOptions.propTypes = {
|
||||
slotPassthrough: PropTypes.object.isRequired,
|
||||
open: PropTypes.bool.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
closeMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ViewingOptions;
|
||||
|
||||
@@ -4,6 +4,7 @@ import ViewingOptions from '../components/ViewingOptions';
|
||||
import { openMenu, closeMenu } from '../actions';
|
||||
import { compose, gql } from 'react-apollo';
|
||||
import { getSlotFragmentSpreads } from 'plugin-api/beta/client/utils';
|
||||
import { mapProps } from 'recompose';
|
||||
|
||||
const slots = ['viewingOptionsSort', 'viewingOptionsFilter'];
|
||||
|
||||
@@ -29,7 +30,14 @@ const withViewingOptionsFragments = withFragments({
|
||||
|
||||
const enhance = compose(
|
||||
connect(mapStateToProps, mapDispatchToProps),
|
||||
withViewingOptionsFragments
|
||||
withViewingOptionsFragments,
|
||||
mapProps(({ root, asset, ...rest }) => ({
|
||||
slotPassthrough: {
|
||||
root,
|
||||
asset,
|
||||
},
|
||||
...rest,
|
||||
}))
|
||||
);
|
||||
|
||||
export default enhance(ViewingOptions);
|
||||
|
||||
Reference in New Issue
Block a user