Merge branch 'master' into slots-debug

This commit is contained in:
Chi Vinh Le
2018-03-20 18:08:59 +01:00
107 changed files with 1371 additions and 1129 deletions
@@ -14,7 +14,7 @@ const buildUserHistory = (userState = {}) => {
return orderBy(
flatten(
Object.keys(userState.status)
.filter(k => k !== '__typename')
.filter(k => !k.startsWith('__'))
.map(k => userState.status[k].history)
),
'created_at',
@@ -25,37 +25,29 @@ class CommentDetails extends Component {
};
render() {
const { data, root, comment, clearHeightCache } = this.props;
const { root, comment, clearHeightCache } = this.props;
const { showDetail } = this.state;
const queryData = {
const slotPassthrough = {
clearHeightCache,
root,
comment,
more: showDetail,
};
return (
<div className={styles.root}>
<IfSlotIsNotEmpty
queryData={queryData}
slot={['adminCommentMoreDetails', 'adminCommentMoreFlagDetails']}
passthrough={slotPassthrough}
>
<a onClick={this.toggleDetail} className={styles.moreDetail}>
{showDetail ? t('modqueue.less_detail') : t('modqueue.more_detail')}
</a>
</IfSlotIsNotEmpty>
<Slot
fill="adminCommentDetailArea"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
more={showDetail}
/>
<Slot fill="adminCommentDetailArea" passthrough={slotPassthrough} />
{showDetail && (
<Slot
fill="adminCommentMoreDetails"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
<Slot fill="adminCommentMoreDetails" passthrough={slotPassthrough} />
)}
</div>
);
@@ -63,7 +55,6 @@ class CommentDetails extends Component {
}
CommentDetails.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object.isRequired,
clearHeightCache: PropTypes.func,
@@ -43,6 +43,9 @@ const CommentLabels = ({
comment,
comment: { className, status, actions, hasParent },
}) => {
const slotPassthrough = {
comment,
};
return (
<div className={cn(className, styles.root)}>
<div className={styles.coreLabels}>
@@ -69,7 +72,7 @@ const CommentLabels = ({
<Slot
className={styles.slot}
fill="adminCommentLabels"
queryData={{ comment }}
passthrough={slotPassthrough}
/>
</div>
);
@@ -98,7 +98,6 @@ class UserDetail extends React.Component {
renderLoaded() {
const {
data,
root,
root: { me, user, totalComments, rejectedComments },
activeTab,
@@ -123,6 +122,11 @@ class UserDetail extends React.Component {
const banned = isBanned(user);
const suspended = isSuspended(user);
const slotPassthrough = {
root,
user,
};
return (
<ClickOutside onClickOutside={modal ? null : hideUserDetail}>
<Drawer
@@ -244,11 +248,7 @@ class UserDetail extends React.Component {
</ul>
</div>
<Slot
fill="userProfile"
data={this.props.data}
queryData={{ root, user }}
/>
<Slot fill="userProfile" passthrough={slotPassthrough} />
<hr />
@@ -301,7 +301,6 @@ class UserDetail extends React.Component {
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
@@ -320,7 +319,6 @@ class UserDetail extends React.Component {
<UserDetailCommentList
user={user}
root={root}
data={data}
loadMore={loadMore}
toggleSelect={toggleSelect}
viewUserDetail={viewUserDetail}
@@ -368,9 +366,7 @@ UserDetail.propTypes = {
bulkReject: PropTypes.func.isRequired,
toggleSelectAll: PropTypes.func.isRequired,
loading: PropTypes.bool.isRequired,
data: PropTypes.shape({
refetch: PropTypes.func.isRequired,
}),
data: PropTypes.object,
activeTab: PropTypes.string.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
@@ -32,13 +32,12 @@ class UserDetailComment extends React.Component {
selected,
toggleSelect,
className,
data,
root: { settings },
} = this.props;
const queryData = { root, comment };
const formatterSettings = {
const slotPassthrough = {
root,
comment,
suspectWords: settings.wordlist.suspect,
bannedWords: settings.wordlist.banned,
body: comment.body,
@@ -89,15 +88,13 @@ class UserDetailComment extends React.Component {
<div className={styles.body}>
<Slot
fill="userDetailCommentContent"
data={data}
className={cn(
styles.commentContent,
'talk-admin-user-detail-comment'
)}
queryData={queryData}
slotSize={1}
size={1}
defaultComponent={CommentFormatter}
{...formatterSettings}
passthrough={slotPassthrough}
/>
<a
className={styles.external}
@@ -130,7 +127,7 @@ class UserDetailComment extends React.Component {
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails data={data} root={root} comment={comment} />
<CommentDetails root={root} comment={comment} />
</li>
);
}
@@ -138,7 +135,6 @@ class UserDetailComment extends React.Component {
UserDetailComment.propTypes = {
selected: PropTypes.bool,
data: PropTypes.object,
user: PropTypes.object.isRequired,
viewUserDetail: PropTypes.func.isRequired,
acceptComment: PropTypes.func.isRequired,
@@ -9,7 +9,6 @@ import ApproveButton from './ApproveButton';
const UserDetailCommentList = props => {
const {
data,
root,
root: { user, comments: { nodes, hasNextPage } },
acceptComment,
@@ -70,7 +69,6 @@ const UserDetailCommentList = props => {
key={comment.id}
user={user}
root={root}
data={data}
comment={comment}
acceptComment={acceptComment}
rejectComment={rejectComment}
@@ -93,7 +91,6 @@ UserDetailCommentList.propTypes = {
root: PropTypes.object.isRequired,
acceptComment: PropTypes.func.isRequired,
rejectComment: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
selectedCommentIds: PropTypes.array.isRequired,
viewUserDetail: PropTypes.any.isRequired,
loadMore: PropTypes.any.isRequired,
@@ -75,7 +75,6 @@ export default class Configure extends Component {
</div>
<div className={styles.mainContent}>
<SectionComponent
data={this.props.data}
root={this.props.root}
settings={this.props.settings}
/>
@@ -88,7 +87,6 @@ export default class Configure extends Component {
Configure.propTypes = {
savePending: PropTypes.func.isRequired,
currentUser: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
@@ -52,7 +52,7 @@ class ModerationSettings extends React.Component {
};
render() {
const { settings, data, root, updatePending, errors } = this.props;
const { settings, slotPassthrough } = this.props;
return (
<ConfigurePage title={t('configure.moderation_settings')}>
@@ -82,13 +82,7 @@ class ModerationSettings extends React.Component {
suspectWords={settings.wordlist.suspect}
onChangeWordlist={this.updateWordlist}
/>
<Slot
fill="adminModerationSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminModerationSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -97,9 +91,8 @@ class ModerationSettings extends React.Component {
ModerationSettings.propTypes = {
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
slotPassthrough: PropTypes.object.isRequired,
};
export default ModerationSettings;
@@ -107,7 +107,7 @@ class StreamSettings extends React.Component {
};
render() {
const { settings, data, root, errors, updatePending } = this.props;
const { settings, slotPassthrough, errors } = this.props;
return (
<ConfigurePage title={t('configure.stream_settings')}>
@@ -220,13 +220,7 @@ class StreamSettings extends React.Component {
</div>
</ConfigureCard>
{/* the above card should be the last one if at all possible because of z-index issues with the selects */}
<Slot
fill="adminStreamSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminStreamSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -235,9 +229,8 @@ class StreamSettings extends React.Component {
StreamSettings.propTypes = {
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
slotPassthrough: PropTypes.object.isRequired,
};
export default StreamSettings;
@@ -34,7 +34,7 @@ class TechSettings extends React.Component {
};
render() {
const { settings, data, root, errors, updatePending } = this.props;
const { settings, slotPassthrough } = this.props;
return (
<ConfigurePage title={t('configure.tech_settings')}>
<Domainlist
@@ -50,13 +50,7 @@ class TechSettings extends React.Component {
onChange={this.updateCustomCssUrl}
/>
</ConfigureCard>
<Slot
fill="adminTechSettings"
data={data}
queryData={{ root, settings }}
updatePending={updatePending}
errors={errors}
/>
<Slot fill="adminTechSettings" passthrough={slotPassthrough} />
</ConfigurePage>
);
}
@@ -64,10 +58,9 @@ class TechSettings extends React.Component {
TechSettings.propTypes = {
updatePending: PropTypes.func.isRequired,
errors: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
slotPassthrough: PropTypes.object.isRequired,
errors: PropTypes.object,
};
export default TechSettings;
@@ -31,7 +31,6 @@ class ConfigureContainer extends Component {
return (
<Configure
currentUser={this.props.currentUser}
data={this.props.data}
root={this.props.root}
settings={this.props.mergedSettings}
canSave={this.props.canSave}
@@ -5,6 +5,7 @@ import ModerationSettings from '../components/ModerationSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
import { mapProps } from 'recompose';
const slots = ['adminModerationSettings'];
@@ -41,5 +42,17 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps)
connect(mapStateToProps, mapDispatchToProps),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
settings,
updatePending,
errors,
},
updatePending,
settings,
errors,
...rest,
}))
)(ModerationSettings);
@@ -5,6 +5,7 @@ import StreamSettings from '../components/StreamSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
import { mapProps } from 'recompose';
const slots = ['adminStreamSettings'];
@@ -42,5 +43,17 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps)
connect(mapStateToProps, mapDispatchToProps),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
settings,
updatePending,
errors,
},
updatePending,
settings,
errors,
...rest,
}))
)(StreamSettings);
@@ -5,6 +5,7 @@ import TechSettings from '../components/TechSettings';
import withFragments from 'coral-framework/hocs/withFragments';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
import { updatePending } from '../../../actions/configure';
import { mapProps } from 'recompose';
const slots = ['adminTechSettings'];
@@ -38,5 +39,17 @@ export default compose(
}
`,
}),
connect(mapStateToProps, mapDispatchToProps)
connect(mapStateToProps, mapDispatchToProps),
mapProps(({ root, settings, updatePending, errors, ...rest }) => ({
slotPassthrough: {
root,
settings,
updatePending,
errors,
},
updatePending,
settings,
errors,
...rest,
}))
)(TechSettings);
@@ -53,7 +53,6 @@ class Comment extends React.Component {
comment,
selected,
className,
data,
root,
root: { settings },
currentAsset,
@@ -62,7 +61,6 @@ class Comment extends React.Component {
} = this.props;
const selectionStateCSS = selected ? 'mdl-shadow--16dp' : 'mdl-shadow--2dp';
const queryData = { root, comment, asset: comment.asset };
const formatterSettings = {
suspectWords: settings.wordlist.suspect,
@@ -70,6 +68,13 @@ class Comment extends React.Component {
body: comment.body,
};
const slotPassthrough = {
clearHeightCache,
root,
comment,
asset: comment.asset,
};
return (
<li
tabIndex={0}
@@ -113,9 +118,7 @@ class Comment extends React.Component {
<CommentLabels comment={comment} />
<Slot
fill="adminCommentInfoBar"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
passthrough={slotPassthrough}
/>
</div>
</div>
@@ -135,13 +138,10 @@ class Comment extends React.Component {
<div className={styles.body}>
<Slot
fill="adminCommentContent"
data={data}
className={cn(styles.commentContent, 'talk-admin-comment')}
clearHeightCache={clearHeightCache}
queryData={queryData}
slotSize={1}
size={1}
defaultComponent={CommentFormatter}
{...formatterSettings}
passthrough={{ ...slotPassthrough, ...formatterSettings }}
/>
<div className={styles.commentContentFooter}>
<a
@@ -171,18 +171,12 @@ class Comment extends React.Component {
onClick={this.reject}
/>
</div>
<Slot
fill="adminSideActions"
data={data}
clearHeightCache={clearHeightCache}
queryData={queryData}
/>
<Slot fill="adminSideActions" passthrough={slotPassthrough} />
</div>
</div>
</CommentAnimatedEdit>
</div>
<CommentDetails
data={data}
root={root}
comment={comment}
clearHeightCache={clearHeightCache}
@@ -220,7 +214,6 @@ Comment.propTypes = {
id: PropTypes.string,
}),
}),
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
selected: PropTypes.bool,
};
@@ -126,7 +126,6 @@ class Moderation extends Component {
render() {
const {
root,
data,
moderation,
viewUserDetail,
activeTab,
@@ -148,6 +147,13 @@ class Moderation extends Component {
count: root[`${queue}Count`],
}));
const slotPassthrough = {
root,
asset,
activeTab,
handleCommentChange,
};
return (
<div>
<ModerationHeader
@@ -171,7 +177,6 @@ class Moderation extends Component {
/>
<ModerationQueue
key={`${activeTab}_${this.props.moderation.sortOrder}`}
data={this.props.data}
root={this.props.root}
currentAsset={asset}
comments={comments.nodes}
@@ -204,13 +209,7 @@ class Moderation extends Component {
closeSearch={this.closeSearch}
storySearchChange={this.props.storySearchChange}
/>
<Slot
data={data}
queryData={{ root, asset }}
activeTab={activeTab}
handleCommentChange={handleCommentChange}
fill="adminModeration"
/>
<Slot fill="adminModeration" passthrough={slotPassthrough} />
</div>
);
}
@@ -344,7 +344,6 @@ class ModerationQueue extends React.Component {
child = (
<div style={style}>
<Comment
data={this.props.data}
root={this.props.root}
comment={comment}
dangling={
@@ -408,7 +407,6 @@ class ModerationQueue extends React.Component {
return (
<div className={styles.root}>
<Comment
data={this.props.data}
root={this.props.root}
key={comment.id}
comment={comment}
@@ -476,7 +474,6 @@ ModerationQueue.propTypes = {
hasNextPage: PropTypes.bool,
comments: PropTypes.array,
activeTab: PropTypes.string.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
currentUserId: PropTypes.string.isRequired,
};
@@ -111,14 +111,14 @@ class IndicatorContainer extends Component {
return null;
}
const slotPassthrough = {
handleCommentChange: this.handleCommentChange,
};
return (
<span>
<Indicator />
<Slot
data={this.props.data}
handleCommentChange={this.handleCommentChange}
fill="adminModerationIndicator"
/>
<Slot fill="adminModerationIndicator" passthrough={slotPassthrough} />
</span>
);
}
@@ -63,6 +63,7 @@ export default class Embed extends React.Component {
} = this.props;
const hasHighlightedComment = !!commentId;
const popupUrl = `login?parentUrl=${encodeURIComponent(parentUrl)}`;
const slotPassthrough = { root };
return (
<div
@@ -84,7 +85,7 @@ export default class Embed extends React.Component {
/>
</IfSlotIsNotEmpty>
<Slot data={data} queryData={{ root }} fill="embed" />
<Slot passthrough={slotPassthrough} fill="embed" />
<ExtendableTabPanel
className="talk-embed-stream-tab-bar"
@@ -94,8 +95,7 @@ export default class Embed extends React.Component {
tabSlot="embedStreamTabs"
tabSlotPrepend="embedStreamTabsPrepend"
tabPaneSlot="embedStreamTabPanes"
slotProps={{ data }}
queryData={{ root }}
slotPassthrough={slotPassthrough}
tabs={this.getTabs()}
tabPanes={[
<TabPane
@@ -138,9 +138,5 @@ Embed.propTypes = {
commentId: PropTypes.string,
root: PropTypes.object,
activeTab: PropTypes.string,
data: PropTypes.shape({
loading: PropTypes.bool,
error: PropTypes.object,
refetch: PropTypes.func,
}).isRequired,
data: PropTypes.object.isRequired,
};
@@ -1,17 +1,12 @@
import React from 'react';
import ExtendableTabPanel from '../components/ExtendableTabPanel';
import { connect } from 'react-redux';
import { TabPane } from 'coral-ui';
import ExtendableTab from '../components/ExtendableTab';
import { getShallowChanges } from 'coral-framework/utils';
import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import { withSlotElements } from 'coral-framework/hocs';
import { compose } from 'recompose';
class ExtendableTabPanelContainer extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
componentDidMount() {
this.handleFallback();
}
@@ -20,24 +15,6 @@ class ExtendableTabPanelContainer extends React.Component {
this.handleFallback(next);
}
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map(
el => el.key
);
const nextKeys = this.getSlotElements(next.tabSlot, next).map(
el => el.key
);
return !isEqual(prevKeys, nextKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
handleFallback(props = this.props) {
if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
props.setActiveTab(props.fallbackTab);
@@ -48,38 +25,26 @@ class ExtendableTabPanelContainer extends React.Component {
return this.getTabElements(props).map(el => el.props.tabId);
}
getSlotElements(slot, props = this.props) {
const { plugins } = this.context;
return plugins.getSlotElements(
slot,
props.reduxState,
props.slotProps,
props.queryData
);
}
getPluginTabElements(props = this.props) {
return this.getSlotTabElements(props.tabSlot);
return props.slotElements[0].map(this.createPluginTabFactory(props));
}
getPluginTabElementsPrepend(props = this.props) {
return this.getSlotTabElements(props.tabSlotPrepend);
return props.slotElements[1].map(this.createPluginTabFactory(props));
}
getSlotTabElements(slot) {
return this.getSlotElements(slot).map(el => {
return (
<ExtendableTab
tabId={el.type.talkPluginName}
key={el.type.talkPluginName}
>
{React.cloneElement(el, {
active: this.props.activeTab === el.type.talkPluginName,
})}
</ExtendableTab>
);
});
}
createPluginTabFactory = (props = this.props) => el => {
return (
<ExtendableTab
tabId={el.type.talkPluginName}
key={el.type.talkPluginName}
>
{React.cloneElement(el, {
active: props.activeTab === el.type.talkPluginName,
})}
</ExtendableTab>
);
};
getTabElements(props = this.props) {
const elements = [...this.getPluginTabElementsPrepend(props)];
@@ -92,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component {
return elements;
}
createPluginTabPane(el) {
return (
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{el}
</TabPane>
);
}
getPluginTabPaneElements(props = this.props) {
return this.getSlotElements(props.tabPaneSlot).map(el => {
return (
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
{el}
</TabPane>
);
});
return props.slotElements[2].map(this.createPluginTabPane);
}
render() {
@@ -132,15 +99,15 @@ ExtendableTabPanelContainer.propTypes = {
tabSlot: PropTypes.string.isRequired,
tabSlotPrepend: PropTypes.string.isRequired,
tabPaneSlot: PropTypes.string.isRequired,
slotProps: PropTypes.object.isRequired,
queryData: PropTypes.object,
slotPassthrough: PropTypes.object,
className: PropTypes.string,
sub: PropTypes.bool,
loading: PropTypes.bool,
};
const mapStateToProps = state => ({
reduxState: state,
});
export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
export default compose(
withSlotElements({
slot: props => [props.tabSlot, props.tabSlotPrepend, props.tabPaneSlot],
passthroughPropName: 'slotPassthrough',
})
)(ExtendableTabPanelContainer);
@@ -7,11 +7,7 @@ class Configure extends React.Component {
render() {
return (
<div className="talk-embed-stream-configuration-container">
<Settings
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>
<Settings root={this.props.root} asset={this.props.asset} />
<hr />
<AssetStatusInfo asset={this.props.asset} />
</div>
@@ -20,7 +16,6 @@ class Configure extends React.Component {
}
Configure.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
};
@@ -25,9 +25,9 @@ class Settings extends React.Component {
onQuestionBoxContentChange,
canSave,
onApply,
slotProps,
queryData,
slotPassthrough,
} = this.props;
return (
<div className={styles.wrapper}>
<div className={styles.container}>
@@ -77,7 +77,7 @@ class Settings extends React.Component {
</div>
)}
</Configuration>
<Slot fill="streamSettings" queryData={queryData} {...slotProps} />
<Slot fill="streamSettings" passthrough={slotPassthrough} />
</div>
</div>
);
@@ -85,8 +85,7 @@ class Settings extends React.Component {
}
Settings.propTypes = {
queryData: PropTypes.object.isRequired,
slotProps: PropTypes.object.isRequired,
slotPassthrough: PropTypes.object.isRequired,
settings: PropTypes.object.isRequired,
canSave: PropTypes.bool.isRequired,
onToggleModeration: PropTypes.func.isRequired,
@@ -13,13 +13,7 @@ class ConfigureContainer extends React.Component {
return <div>{this.props.data.error.message}</div>;
}
return (
<Configure
data={this.props.data}
root={this.props.root}
asset={this.props.asset}
/>
);
return <Configure root={this.props.root} asset={this.props.asset} />;
}
}
@@ -57,17 +57,24 @@ class SettingsContainer extends React.Component {
const {
mergedSettings,
canSave,
data,
root,
asset,
errors,
updatePending,
} = this.props;
const slotPassthrough = {
root,
asset,
settings: mergedSettings,
updatePending,
errors,
};
return (
<Settings
settings={mergedSettings}
queryData={{ root, asset, settings: mergedSettings }}
slotProps={{ data, updatePending, errors }}
slotPassthrough={slotPassthrough}
savePending={this.savePending}
onToggleModeration={this.toggleModeration}
onTogglePremodLinks={this.togglePremodLinks}
@@ -82,7 +89,6 @@ class SettingsContainer extends React.Component {
}
SettingsContainer.propTypes = {
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
asset: PropTypes.object.isRequired,
pending: PropTypes.object.isRequired,
@@ -22,9 +22,9 @@ class Comment extends React.Component {
};
render() {
const { comment, data, root } = this.props;
const { comment, root } = this.props;
const reactionCount = getTotalReactionsCount(comment.action_summaries);
const queryData = { root, comment, asset: comment.asset };
const slotPassthrough = { root, comment, asset: comment.asset };
return (
<div className={styles.myComment}>
@@ -33,8 +33,7 @@ class Comment extends React.Component {
fill="commentContent"
defaultComponent={CommentContent}
className={cn(styles.commentBody, 'my-comment-body')}
data={data}
queryData={queryData}
passthrough={slotPassthrough}
/>
<div className={cn(styles.commentSummary, 'comment-summary')}>
<span
@@ -98,9 +97,10 @@ class Comment extends React.Component {
fill="historyCommentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-history-comment-published-date'}
created_at={comment.created_at}
data={data}
queryData={queryData}
passthrough={{
created_at: comment.created_at,
...slotPassthrough,
}}
inline
/>
</li>
@@ -114,7 +114,6 @@ class Comment extends React.Component {
Comment.propTypes = {
comment: PropTypes.object.isRequired,
navigate: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
};
@@ -22,7 +22,7 @@ class CommentHistory extends React.Component {
};
render() {
const { navigate, comments, data, root } = this.props;
const { navigate, comments, root } = this.props;
if (!comments.nodes.length) {
return <BlankCommentHistory />;
}
@@ -33,7 +33,6 @@ class CommentHistory extends React.Component {
return (
<Comment
key={i}
data={data}
root={root}
comment={comment}
navigate={navigate}
@@ -56,7 +55,6 @@ CommentHistory.propTypes = {
comments: PropTypes.object.isRequired,
loadMore: PropTypes.func,
navigate: PropTypes.func,
data: PropTypes.object,
root: PropTypes.object,
};
@@ -1,57 +1,27 @@
import React from 'react';
import PropTypes from 'prop-types';
import Slot from 'coral-framework/components/Slot';
import CommentHistory from '../containers/CommentHistory';
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
import { Tab, TabPane } from 'coral-ui';
import styles from './Profile.css';
import t from 'coral-framework/services/i18n';
import TabPanel from '../containers/TabPanel';
const Profile = ({
username,
emailAddress,
data,
root,
activeTab,
setActiveTab,
}) => (
<div className="talk-my-profile talk-profile-container">
<div className={styles.userInfo}>
<h2 className={styles.username}>{username}</h2>
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
const Profile = ({ username, emailAddress, root, slotPassthrough }) => {
return (
<div className="talk-my-profile talk-profile-container">
<div className={styles.userInfo}>
<h2 className={styles.username}>{username}</h2>
{emailAddress ? <p className={styles.email}>{emailAddress}</p> : null}
</div>
<Slot fill="profileSections" passthrough={slotPassthrough} />
<TabPanel root={root} slotPassthrough={slotPassthrough} />
</div>
<Slot fill="profileSections" data={data} queryData={{ root }} />
<ExtendableTabPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
fallbackTab="comments"
tabSlot="profileTabs"
tabSlotPrepend="profileTabsPrepend"
tabPaneSlot="profileTabPanes"
slotProps={{ data }}
queryData={{ root }}
tabs={[
<Tab key="comments" tabId="comments">
{t('framework.my_comments')}
</Tab>,
]}
tabPanes={[
<TabPane key="comments" tabId="comments">
<CommentHistory data={data} root={root} />
</TabPane>,
]}
sub
/>
</div>
);
);
};
Profile.propTypes = {
username: PropTypes.string,
emailAddress: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
slotPassthrough: PropTypes.object,
};
export default Profile;
@@ -0,0 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Slot } from 'coral-framework/components';
class Settings extends React.Component {
render() {
const { root } = this.props;
const slotPassthrough = { root };
return (
<div>
<Slot fill="profileSettings" passthrough={slotPassthrough} />
</div>
);
}
}
Settings.propTypes = {
root: PropTypes.object,
};
export default Settings;
@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import CommentHistory from '../containers/CommentHistory';
import ExtendableTabPanel from '../../../containers/ExtendableTabPanel';
import { Tab, TabPane } from 'coral-ui';
import t from 'coral-framework/services/i18n';
import Settings from '../containers/Settings';
const TabPanel = ({
root,
activeTab,
setActiveTab,
showSettingsTab,
slotPassthrough,
}) => {
const tabs = [
<Tab key="comments" tabId="comments">
{t('framework.my_comments')}
</Tab>,
];
if (showSettingsTab) {
tabs.push(
<Tab key="settings" tabId="settings">
{t('profile_settings')}
</Tab>
);
}
return (
<ExtendableTabPanel
activeTab={activeTab}
setActiveTab={setActiveTab}
fallbackTab="comments"
tabSlot="profileTabs"
tabSlotPrepend="profileTabsPrepend"
tabPaneSlot="profileTabPanes"
slotPassthrough={slotPassthrough}
tabs={tabs}
tabPanes={[
<TabPane key="comments" tabId="comments">
<CommentHistory root={root} />
</TabPane>,
<TabPane key="settings" tabId="settings">
<Settings root={root} />
</TabPane>,
]}
sub
/>
);
};
TabPanel.propTypes = {
root: PropTypes.object,
slotPassthrough: PropTypes.object,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
showSettingsTab: PropTypes.bool.isRequired,
};
export default TabPanel;
@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { compose, gql } from 'react-apollo';
import CommentHistory from '../components/CommentHistory';
import Comment from './Comment';
import { withFragments } from 'coral-framework/hocs';
import { withFragments, withFetchMore } from 'coral-framework/hocs';
import { appendNewNodes } from 'plugin-api/beta/client/utils';
import update from 'immutability-helper';
@@ -16,7 +16,7 @@ class CommentHistoryContainer extends Component {
};
loadMore = () => {
return this.props.data.fetchMore({
return this.props.fetchMore({
query: LOAD_MORE_QUERY,
variables: {
limit: 5,
@@ -43,7 +43,6 @@ class CommentHistoryContainer extends Component {
return (
<CommentHistory
comments={this.props.root.me.comments}
data={this.props.data}
root={this.props.root}
loadMore={this.loadMore}
navigate={this.navigate}
@@ -57,8 +56,8 @@ CommentHistoryContainer.contextTypes = {
};
CommentHistoryContainer.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
fetchMore: PropTypes.func.isRequired,
};
const LOAD_MORE_QUERY = gql`
@@ -99,5 +98,6 @@ const mapStateToProps = state => ({
export default compose(
connect(mapStateToProps, null),
withCommentHistoryFragments
withCommentHistoryFragments,
withFetchMore
)(CommentHistoryContainer);
@@ -7,11 +7,10 @@ import { withQuery } from 'coral-framework/hocs';
import NotLoggedIn from '../components/NotLoggedIn';
import { Spinner } from 'coral-ui';
import Profile from '../components/Profile';
import CommentHistory from './CommentHistory';
import TabPanel from './TabPanel';
import { getDefinitionName } from 'coral-framework/utils';
import { showSignInDialog } from 'coral-embed-stream/src/actions/login';
import { setActiveTab } from '../../../actions/profile';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
class ProfileContainer extends Component {
@@ -23,7 +22,7 @@ class ProfileContainer extends Component {
}
render() {
const { currentUser, showSignInDialog, root, data } = this.props;
const { currentUser, showSignInDialog, root } = this.props;
const { me } = this.props.root;
const loading = this.props.data.loading;
@@ -41,15 +40,14 @@ class ProfileContainer extends Component {
const localProfile = currentUser.profiles.find(p => p.provider === 'local');
const emailAddress = localProfile && localProfile.id;
const slotPassthrough = { root };
return (
<Profile
username={me.username}
emailAddress={emailAddress}
data={data}
root={root}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
slotPassthrough={slotPassthrough}
/>
);
}
@@ -60,16 +58,9 @@ ProfileContainer.propTypes = {
root: PropTypes.object,
currentUser: PropTypes.object,
showSignInDialog: PropTypes.func,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
};
const slots = [
'profileSections',
'profileTabs',
'profileTabsPrepend',
'profileTabPanes',
];
const slots = ['profileSections'];
const withProfileQuery = withQuery(
gql`
@@ -78,10 +69,10 @@ const withProfileQuery = withQuery(
id
username
}
...${getDefinitionName(CommentHistory.fragments.root)}
...${getDefinitionName(TabPanel.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentHistory.fragments.root}
${TabPanel.fragments.root}
`,
{
options: {
@@ -92,11 +83,10 @@ const withProfileQuery = withQuery(
const mapStateToProps = state => ({
currentUser: state.auth.user,
activeTab: state.profile.activeTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ showSignInDialog, setActiveTab }, dispatch);
bindActionCreators({ showSignInDialog }, dispatch);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
@@ -0,0 +1,26 @@
import React from 'react';
import { compose, gql } from 'react-apollo';
import Settings from '../components/Settings';
import { withFragments } from 'coral-framework/hocs';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
const slots = ['profileSettings'];
class SettingsContainer extends React.Component {
render() {
return <Settings {...this.props} />;
}
}
const enhance = compose(
withFragments({
root: gql`
fragment TalkEmbedStream_ProfileSettings_root on RootQuery {
__typename
${getSlotFragmentSpreads(slots, 'root')}
}
`,
})
);
export default enhance(SettingsContainer);
@@ -0,0 +1,64 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { compose, gql } from 'react-apollo';
import { bindActionCreators } from 'redux';
import { withSlotElements, withFragments } from 'coral-framework/hocs';
import Settings from './Settings';
import CommentHistory from './CommentHistory';
import { getDefinitionName } from 'coral-framework/utils';
import TabPanel from '../components/TabPanel';
import { setActiveTab } from '../../../actions/profile';
import { getSlotFragmentSpreads } from 'coral-framework/utils';
class TabPanelContainer extends Component {
render() {
return (
<TabPanel
root={this.props.root}
slotPassthrough={this.props.slotPassthrough}
activeTab={this.props.activeTab}
setActiveTab={this.props.setActiveTab}
showSettingsTab={this.props.profileSettingsSlotElements.length > 0}
/>
);
}
}
TabPanelContainer.propTypes = {
root: PropTypes.object,
slotPassthrough: PropTypes.object,
activeTab: PropTypes.string.isRequired,
setActiveTab: PropTypes.func.isRequired,
profileSettingsSlotElements: PropTypes.array.isRequired,
};
const slots = ['profileTabs', 'profileTabsPrepend', 'profileTabPanes'];
const mapStateToProps = state => ({
activeTab: state.profile.activeTab,
});
const mapDispatchToProps = dispatch =>
bindActionCreators({ setActiveTab }, dispatch);
export default compose(
withFragments({
root: gql`
fragment TalkEmbedStream_ProfileTabPanel_root on RootQuery {
__typename
...${getDefinitionName(CommentHistory.fragments.root)}
...${getDefinitionName(Settings.fragments.root)}
${getSlotFragmentSpreads(slots, 'root')}
}
${CommentHistory.fragments.root}
${Settings.fragments.root}
`,
}),
connect(mapStateToProps, mapDispatchToProps),
withSlotElements({
slot: 'profileSettings',
propName: 'profileSettingsSlotElements',
passthroughPropName: 'slotPassthrough',
})
)(TabPanelContainer);
@@ -126,7 +126,6 @@ class AllCommentsPane extends React.Component {
render() {
const {
data,
root,
comments,
commentClassNames,
@@ -164,7 +163,6 @@ class AllCommentsPane extends React.Component {
return (
<Comment
commentClassNames={commentClassNames}
data={data}
root={root}
disableReply={disableReply}
setActiveReplyBox={setActiveReplyBox}
@@ -205,7 +203,6 @@ class AllCommentsPane extends React.Component {
}
AllCommentsPane.propTypes = {
data: PropTypes.object,
root: PropTypes.object,
comments: PropTypes.object,
commentClassNames: PropTypes.array,
@@ -181,7 +181,6 @@ export default class Comment extends React.Component {
}),
charCountEnable: PropTypes.bool.isRequired,
maxCharCount: PropTypes.number,
data: PropTypes.object,
root: PropTypes.object,
loadMore: PropTypes.func,
postDontAgree: PropTypes.func,
@@ -417,7 +416,6 @@ export default class Comment extends React.Component {
{view.map(reply => {
return (
<CommentContainer
data={this.props.data}
root={this.props.root}
setActiveReplyBox={setActiveReplyBox}
disableReply={disableReply}
@@ -487,7 +485,6 @@ export default class Comment extends React.Component {
renderComment() {
const {
asset,
data,
root,
depth,
comment,
@@ -535,12 +532,8 @@ export default class Comment extends React.Component {
);
// props that are passed down the slots.
const slotProps = {
data,
const slotPassthrough = {
depth,
};
const queryData = {
root,
asset,
comment,
@@ -551,8 +544,7 @@ export default class Comment extends React.Component {
<Slot
className={cn(styles.commentAvatar, 'talk-stream-comment-avatar')}
fill="commentAvatar"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
@@ -573,8 +565,7 @@ export default class Comment extends React.Component {
className={cn(styles.username, 'talk-stream-comment-user-name')}
fill="commentAuthorName"
defaultComponent={CommentAuthorName}
queryData={queryData}
{...slotProps}
passthrough={slotPassthrough}
/>
<div
@@ -591,8 +582,7 @@ export default class Comment extends React.Component {
'talk-stream-comment-author-tags'
)}
fill="commentAuthorTags"
queryData={queryData}
{...slotProps}
passthrough={slotPassthrough}
inline
/>
</div>
@@ -607,9 +597,10 @@ export default class Comment extends React.Component {
fill="commentTimestamp"
defaultComponent={CommentTimestamp}
className={'talk-stream-comment-published-date'}
created_at={comment.created_at}
queryData={queryData}
{...slotProps}
passthrough={{
created_at: comment.created_at,
...slotPassthrough,
}}
/>
{comment.editing && comment.editing.edited ? (
<span>
@@ -624,8 +615,7 @@ export default class Comment extends React.Component {
<Slot
className={styles.commentInfoBar}
fill="commentInfoBar"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
/>
{isActive &&
@@ -665,9 +655,8 @@ export default class Comment extends React.Component {
fill="commentContent"
className="talk-stream-comment-content"
defaultComponent={CommentContent}
{...slotProps}
queryData={queryData}
slotSize={1}
size={1}
passthrough={slotPassthrough}
/>
</div>
)}
@@ -678,8 +667,7 @@ export default class Comment extends React.Component {
<div className="talk-embed-stream-comment-actions-container-left commentActionsLeft comment__action-container">
<Slot
fill="commentReactions"
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
@@ -696,9 +684,7 @@ export default class Comment extends React.Component {
<div className="talk-embed-stream-comment-actions-container-right commentActionsRight comment__action-container">
<Slot
fill="commentActions"
wrapperComponent={ActionButton}
{...slotProps}
queryData={queryData}
passthrough={slotPassthrough}
inline
/>
<ActionButton>
@@ -43,20 +43,13 @@ export default class DraftArea extends React.Component {
charCountEnable,
maxCharCount,
onChange,
queryData,
isReply,
registerHook,
unregisterHook,
root,
comment,
} = this.props;
const tASettings = {
value,
placeholder,
id,
onChange,
rows,
disabled,
isReply,
};
return (
<div>
<div
@@ -69,10 +62,19 @@ export default class DraftArea extends React.Component {
fill="draftArea"
defaultComponent={DraftAreaContent}
className={styles.content}
queryData={queryData}
registerHook={this.props.registerHook}
unregisterHook={this.props.unregisterHook}
{...tASettings}
passthrough={{
root,
comment,
registerHook,
unregisterHook,
value,
placeholder,
id,
onChange,
rows,
disabled,
isReply,
}}
/>
<Slot fill="commentInputArea" />
</div>
@@ -96,7 +98,8 @@ DraftArea.propTypes = {
onChange: PropTypes.func,
disabled: PropTypes.bool,
rows: PropTypes.number,
queryData: PropTypes.object.isRequired,
root: PropTypes.object.isRequired,
comment: PropTypes.object,
registerHook: PropTypes.func,
unregisterHook: PropTypes.func,
isReply: PropTypes.bool,
@@ -40,7 +40,6 @@ class Stream extends React.Component {
renderHighlightedComment() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
@@ -91,7 +90,6 @@ class Stream extends React.Component {
</div>
<Comment
data={data}
root={root}
commentClassNames={commentClassNames}
setActiveReplyBox={setActiveReplyBox}
@@ -122,7 +120,6 @@ class Stream extends React.Component {
renderExtendableTabPanel() {
const {
data,
root,
activeReplyBox,
setActiveReplyBox,
@@ -147,15 +144,14 @@ class Stream extends React.Component {
loading,
} = this.props;
const slotProps = { data };
const slotQueryData = { root, asset };
const slotPassthrough = { root, asset };
// `key` of `ExtendableTabPanel` depends on sorting so that we always reset
// the state when changing sorting.
return (
<div className={cn('talk-stream-tab-container', styles.tabContainer)}>
<div className={cn('talk-stream-filter-wrapper', styles.filterWrapper)}>
<Slot fill="streamFilter" queryData={slotQueryData} {...slotProps} />
<Slot fill="streamFilter" passthrough={slotPassthrough} />
</div>
<ExtendableTabPanel
@@ -166,8 +162,7 @@ class Stream extends React.Component {
tabSlot="streamTabs"
tabSlotPrepend="streamTabsPrepend"
tabPaneSlot="streamTabPanes"
slotProps={slotProps}
queryData={slotQueryData}
slotPassthrough={slotPassthrough}
loading={loading}
tabs={
<Tab tabId={'all'} key="all">
@@ -180,7 +175,6 @@ class Stream extends React.Component {
tabPanes={
<TabPane tabId="all" key="all">
<AllCommentsPane
data={data}
root={root}
asset={asset}
comments={comments}
@@ -212,7 +206,6 @@ class Stream extends React.Component {
render() {
const {
data,
root,
appendItemArray,
asset,
@@ -243,13 +236,13 @@ class Stream extends React.Component {
!changedUsername &&
!highlightedComment) ||
keepCommentBox);
const slotProps = { data };
const slotQueryData = { root, asset };
if (highlightedComment === null) {
return <StreamError>{t('stream.comment_not_found')}</StreamError>;
}
const slotPassthrough = { root, asset };
return (
<div id="stream" className={styles.root}>
{open ? (
@@ -263,11 +256,7 @@ class Stream extends React.Component {
content={asset.settings.questionBoxContent}
icon={asset.settings.questionBoxIcon}
>
<Slot
fill="streamQuestionArea"
queryData={slotQueryData}
{...slotProps}
/>
<Slot fill="streamQuestionArea" passthrough={slotPassthrough} />
</QuestionBox>
)}
{!banned &&
@@ -304,7 +293,7 @@ class Stream extends React.Component {
<p>{asset.settings.closedMessage}</p>
)}
<Slot fill="stream" queryData={slotQueryData} {...slotProps} />
<Slot fill="stream" passthrough={slotPassthrough} />
{currentUser && (
<ModerationLink
@@ -324,7 +313,6 @@ class Stream extends React.Component {
Stream.propTypes = {
asset: PropTypes.object,
activeStreamTab: PropTypes.string,
data: PropTypes.object,
root: PropTypes.object,
activeReplyBox: PropTypes.string,
setActiveReplyBox: PropTypes.func,
@@ -190,9 +190,11 @@ class CommentBox extends React.Component {
buttonContainerStart={
<Slot
fill="commentInputDetailArea"
registerHook={this.registerHook}
unregisterHook={this.unregisterHook}
isReply={isReply}
passthrough={{
registerHook: this.registerHook,
unregisterHook: this.unregisterHook,
isReply,
}}
inline
/>
}
@@ -42,11 +42,10 @@ class DraftAreaContainer extends React.Component {
}
render() {
const queryData = { comment: this.props.comment, root: this.props.root };
return (
<DraftArea
queryData={queryData}
root={this.props.root}
comment={this.props.comment}
value={this.props.value}
placeholder={this.props.placeholder}
id={this.props.id}
@@ -1,39 +1,14 @@
import React, { Children } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getShallowChanges } from 'coral-framework/utils';
import { withSlotElements, withCompatPassthrough } from '../hocs';
import { compose } from 'recompose';
class IfSlotIsEmpty extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
isSlotEmpty(props = this.props) {
const {
slot,
className: _a,
reduxState,
component: _b = 'div',
children: _c,
queryData,
...rest
} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every(slot =>
this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)
);
const { slotElements } = props;
return slotElements.length === 0
? false
: slotElements.every(elements => elements.length === 0);
}
render() {
@@ -44,10 +19,15 @@ class IfSlotIsEmpty extends React.Component {
IfSlotIsEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
children: PropTypes.node.isRequired,
passthrough: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
reduxState: state,
});
const omitProps = ['slot', 'children'];
export default connect(mapStateToProps, null)(IfSlotIsEmpty);
export default compose(
withCompatPassthrough(omitProps),
withSlotElements({
slot: props => props.slot,
})
)(IfSlotIsEmpty);
@@ -1,39 +1,14 @@
import React, { Children } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getShallowChanges } from 'coral-framework/utils';
import { withSlotElements, withCompatPassthrough } from '../hocs';
import { compose } from 'recompose';
class IfSlotIsNotEmpty extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
isSlotEmpty(props = this.props) {
const {
slot,
className: _a,
reduxState,
component: _b = 'div',
children: _c,
queryData,
...rest
} = props;
const slots = Array.isArray(slot) ? slot : [slot];
return slots.every(slot =>
this.context.plugins.isSlotEmpty(slot, reduxState, rest, queryData)
);
const { slotElements } = props;
return slotElements.length === 0
? false
: slotElements.every(elements => elements.length === 0);
}
render() {
@@ -44,10 +19,15 @@ class IfSlotIsNotEmpty extends React.Component {
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
children: PropTypes.node.isRequired,
passthrough: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
reduxState: state,
});
const omitProps = ['slot', 'children'];
export default connect(mapStateToProps, null)(IfSlotIsNotEmpty);
export default compose(
withCompatPassthrough(omitProps),
withSlotElements({
slot: props => props.slot,
})
)(IfSlotIsNotEmpty);
+33 -86
View File
@@ -4,106 +4,29 @@ import styles from './Slot.css';
import { connect } from 'react-redux';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
import { getShallowChanges } from 'coral-framework/utils';
import omit from 'lodash/omit';
import merge from 'lodash/merge';
const emptyConfig = {};
import { withSlotElements, withCompatPassthrough } from '../hocs';
import { compose } from 'recompose';
class Slot extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
if (changes.length === 1 && changes[0] === 'reduxState') {
const prevChildrenKeys = this.getChildren(this.props).map(
child => child.key
);
const nextChildrenKeys = this.getChildren(next).map(child => child.key);
return !isEqual(prevChildrenKeys, nextChildrenKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
getSlotProps(props = this.props) {
return omit(props, [
'fill',
'inline',
'className',
'reduxState',
'slotSize',
'defaultComponent',
'queryData',
'childFactory',
'component',
]);
}
getChildren(props = this.props) {
const { slotSize = 0 } = props;
const { plugins } = this.context;
return plugins.getSlotElements(
props.fill,
props.reduxState,
this.getSlotProps(props),
props.queryData,
{ slotSize }
);
}
render() {
const {
inline = false,
className,
reduxState,
debug,
component: Component,
childFactory,
defaultComponent: DefaultComponent,
queryData,
fill,
} = this.props;
const { plugins } = this.context;
let children = this.getChildren();
// @Deprecated plugin_config
const pluginsConfig =
merge(
get(reduxState, 'config.plugins_config'),
get(reduxState, 'config.plugin_config')
) || emptyConfig;
if (children.length === 0 && DefaultComponent) {
const props = plugins.getSlotComponentProps(
DefaultComponent,
reduxState,
this.getSlotProps(this.props),
queryData
);
children = <DefaultComponent {...props} />;
}
let children = this.props.slotElements;
if (childFactory) {
children = children.map(childFactory);
}
// console.log('pluginsConfig', pluginsConfig);
return (
<Component
className={cn(
{
[styles.inline]: inline,
[styles.debug]: pluginsConfig.debug,
},
{ [styles.inline]: inline, [styles.debug]: debug },
className,
`talk-slot-${kebabCase(fill)}`
)}
@@ -122,13 +45,14 @@ Slot.propTypes = {
fill: PropTypes.string.isRequired,
inline: PropTypes.bool,
className: PropTypes.string,
reduxState: PropTypes.object,
debug: PropTypes.bool,
slotElements: PropTypes.arrayOf(PropTypes.element).isRequired,
defaultComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
/**
* Specifies the number of children that can fill the slot.
*/
slotSize: PropTypes.number,
size: PropTypes.number,
/**
* You may specify the component to use as the root wrapper.
@@ -137,8 +61,12 @@ Slot.propTypes = {
component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
// props coming from graphql must be passed through this property.
// @Deprecated
queryData: PropTypes.object,
// props that are passed to all Slot Components
passthrough: PropTypes.object,
/**
* You may need to apply reactive updates to a child as it is exiting.
* This is generally done by using `cloneElement` however in the case of an exiting
@@ -152,8 +80,27 @@ Slot.propTypes = {
childFactory: PropTypes.func,
};
const omitProps = [
'fill',
'inline',
'className',
'size',
'defaultComponent',
'queryData',
'childFactory',
'component',
];
const mapStateToProps = state => ({
reduxState: state,
debug: get(state, 'config.plugins_config.debug'),
});
export default connect(mapStateToProps, null)(Slot);
export default compose(
withCompatPassthrough(omitProps),
withSlotElements({
slot: props => props.fill,
size: props => props.size,
defaultComponent: props => props.defaultComponent,
}),
connect(mapStateToProps, null)
)(Slot);
+7
View File
@@ -12,6 +12,13 @@ export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export { default as withPopupAuthHandler } from './withPopupAuthHandler';
export { default as withEnumValues } from './withEnumValues';
export { default as withCompatPassthrough } from './withCompatPassthrough';
export { default as withSlotElements } from './withSlotElements';
export { default as withVariables } from './withVariables';
export { default as WithRefetch } from './withRefetch';
export { default as withFetchMore } from './withFetchMore';
export { default as withSubscribeToMore } from './withSubscribeToMore';
export { default as withGraphQLExtension } from './withGraphQLExtension';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
@@ -0,0 +1,53 @@
import withProps from 'recompose/withProps';
import omit from 'lodash/omit';
function getPassthrough(props, omitProps) {
const slotProps = omit(props, [...omitProps, 'passthrough']);
// @Deprecated
if (process.env.NODE_ENV !== 'production') {
if (Object.keys(slotProps).length) {
/* eslint-disable no-console */
console.warn(
`Slot '${
props.fill
}' passing through unknown props is deprecated, please use 'passthrough' instead`,
slotProps
);
/* eslint-enable no-console */
}
}
if (props.passthrough) {
return props.passthrough;
}
if (props.queryData) {
if (process.env.NODE_ENV !== 'production') {
/* eslint-disable no-console */
console.warn(
`Slot '${
props.fill
}' property 'queryData' is deprecated, please use 'passthrough' instead`
);
/* eslint-enable no-console */
}
return {
...props.queryData,
...slotProps,
};
}
return slotProps;
}
/**
* @Deprecated
* withCompatPassthrough is a compatibility HOC that supports our old
* API which puts unknown props and `queryData` to `passhtrough` to be
* used with HOC `withSlotElements`.
*/
export default omitProps =>
withProps(props => ({
passthrough: getPassthrough(props, omitProps),
}));
@@ -0,0 +1,27 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { Subscriber } from 'react-broadcast';
import get from 'lodash/get';
/**
* WithFetchMore provides a property `fetchMore` to the wrapped component.
* Calling `fetchMore` is the same as calling `data.fetchMore` from the
* Apollo React API.
*/
export default hoistStatics(WrappedComponent => {
class WithFetchMore extends React.Component {
render() {
return (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
fetchMore={get(data, 'fetchMore')}
/>
)}
</Subscriber>
);
}
}
return WithFetchMore;
});
+4 -10
View File
@@ -6,27 +6,21 @@ import { getShallowChanges } from 'coral-framework/utils';
import PropTypes from 'prop-types';
import union from 'lodash/union';
// TODO: Should not depend on `props.data`
// Currently necessary because of this https://github.com/apollographql/graphql-anywhere/issues/38
function filter(doc, data, variables) {
function filter(doc, data) {
const resolver = (fieldName, root, args, context, info) => {
return root[info.resultKey];
};
return graphql(resolver, doc, data, null, variables);
return graphql(resolver, doc, data, null, null, { includeAll: true });
}
// filterProps returns only the property as defined in the fragments.
// TODO: Should not depend on `props.data`
function filterProps(props, fragments) {
const filtered = {};
Object.keys(fragments).forEach(key => {
if (!(key in props)) {
if (!(key in props) || props[key] === undefined) {
return;
}
filtered[key] = props.data
? filter(fragments[key], props[key], props.data.variables)
: props[key];
filtered[key] = filter(fragments[key], props[key]);
});
return filtered;
}
@@ -0,0 +1,19 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
/**
* WithGraphQLExtension adds graphql configuration to the
* GraphQL registry, only works on Components used
* directly in a Slot.
*/
export default extension =>
hoistStatics(WrappedComponent => {
class WithGraphQLExtension extends React.Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
WrappedComponent.graphqlExtension = extension;
return WithGraphQLExtension;
});
+19 -4
View File
@@ -11,6 +11,8 @@ import { getOperationName } from 'apollo-client/queries/getFromAST';
import throttle from 'lodash/throttle';
import get from 'lodash/get';
import { notify } from 'coral-framework/actions/notification';
import { Broadcast } from 'react-broadcast';
import { compose } from 'recompose';
const withSkipOnErrors = reducer => (prev, action, ...rest) => {
if (
@@ -43,6 +45,15 @@ function networkStatusToString(networkStatus) {
}
}
// When wrapped broadcast all data changes to channel "queryData".
const withBroadcaster = WrappedComponent => props => (
/* eslint-disable react/prop-types */
<Broadcast channel="queryData" value={props.data}>
<WrappedComponent {...props} />
</Broadcast>
/* eslint-enable react/prop-types */
);
const createHOC = (document, config, { notifyOnError = true }) =>
hoistStatics(WrappedComponent => {
return class WithQuery extends React.Component {
@@ -283,6 +294,7 @@ const createHOC = (document, config, { notifyOnError = true }) =>
props: args => {
const nextData = this.nextData(args.data);
const { root } = separateDataAndRoot(args.data);
if (config.props) {
// Custom props, in this case we just pass the wrapped args to it.
return config.props({
@@ -323,10 +335,13 @@ const createHOC = (document, config, { notifyOnError = true }) =>
if (!this.memoized) {
this.resolvedDocument = this.resolveDocument(document);
this.name = getDefinitionName(this.resolvedDocument);
this.memoized = graphql(this.resolvedDocument, {
...this.wrappedConfig,
options: this.wrappedOptions,
})(WrappedComponent);
this.memoized = compose(
graphql(this.resolvedDocument, {
...this.wrappedConfig,
options: this.wrappedOptions,
}),
withBroadcaster
)(WrappedComponent);
}
return this.memoized;
};
@@ -0,0 +1,23 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { Subscriber } from 'react-broadcast';
import get from 'lodash/get';
/**
* WithRefetch provides a property `refetch` to the wrapped component.
* Calling refetch will perform a refetch of the parent Query.
*/
export default hoistStatics(WrappedComponent => {
class WithRefetch extends React.Component {
render() {
return (
<Subscriber channel="queryData">
{data => (
<WrappedComponent {...this.props} refetch={get(data, 'refetch')} />
)}
</Subscriber>
);
}
}
return WithRefetch;
});
@@ -0,0 +1,195 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import isEqual from 'lodash/isEqual';
import { getShallowChanges } from 'coral-framework/utils';
import { compose } from 'recompose';
import hoistStatics from 'recompose/hoistStatics';
import isFunction from 'lodash/isFunction';
function resolvePrimitiveOrFunction(primitiveOrFunction, props) {
if (isFunction(primitiveOrFunction)) {
return primitiveOrFunction(props);
}
return primitiveOrFunction;
}
const createHOC = ({
slot,
defaultComponent = null,
passthroughPropName = 'passthrough',
size = null,
propName = 'slotElements',
}) =>
hoistStatics(WrappedComponent => {
return class withSlotElements extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
static propTypes = {
reduxState: PropTypes.object,
};
getSlots(props = this.props) {
const tmp = resolvePrimitiveOrFunction(slot, props);
if (Array.isArray(tmp)) {
return tmp;
}
return [tmp];
}
getDefaultComponents(props = this.props, fill = 1) {
const tmp = resolvePrimitiveOrFunction(defaultComponent, props);
if (Array.isArray(tmp)) {
return tmp;
}
return new Array(fill).fill(tmp);
}
getSizes(props = this.props, fill = 1) {
const tmp = resolvePrimitiveOrFunction(size, props);
if (Array.isArray(tmp)) {
return tmp;
}
return new Array(fill).fill(tmp);
}
getPassthrough(props = this.props) {
return passthroughPropName ? props[passthroughPropName] : null;
}
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
// Handle special `passthrough` props.
if (passthroughPropName) {
const passthroughIndex = changes.indexOf(passthroughPropName);
if (passthroughIndex !== -1) {
if (!this.props[passthroughPropName] || next[passthroughPropName]) {
return true;
}
if (
getShallowChanges(
this.props[passthroughPropName],
next[passthroughPropName]
).lenght === 0
) {
changes.splice(passthroughIndex, 1);
}
}
}
if (changes.length === 1 && changes[0] === 'reduxState') {
const prevChildrenKeys = this.getSlotElements(this.props).map(
child => child.key
);
const nextChildrenKeys = this.getSlotElements(next).map(
child => child.key
);
return !isEqual(prevChildrenKeys, nextChildrenKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
/**
* Returns slot elements for configured slots. If only one slot is given
* slot elements are returned directly. If more than one slot is specified
* returns an array of slot elements.
*/
getSlotElements(props = this.props) {
const { plugins } = this.context;
const slots = this.getSlots(props);
const sizes = this.getSizes(props, slots.length);
const defaultComponents = this.getDefaultComponents(
props,
slots.length
);
const slotPassthrough = this.getPassthrough(props);
if (process.env.NODE_ENV !== 'production') {
if (slotPassthrough.data && slotPassthrough.data.refetch) {
/* eslint-disable no-console */
console.warn(
'Slots no longer need `data` property.',
'Plugins can use the new HOCs `withRefetch`,',
'`withFetchMore`, `withSubscribeToMore` and `withVariables` instead.',
'Affected slots: ',
slots
);
/* eslint-enable no-console */
}
}
const elements = [];
slots.forEach((s, i) => {
const DefaultComponent = defaultComponents[i];
const size = sizes[i];
const slotElements = plugins.getSlotElements(
s,
props.reduxState,
slotPassthrough,
{ size }
);
if (slotElements.length === 0 && DefaultComponent) {
const p = plugins.getSlotComponentProps(
DefaultComponent,
props.reduxState,
slotPassthrough
);
slotElements.push(<DefaultComponent key="default" {...p} />);
}
elements.push(slotElements);
});
if (elements.length === 1) {
return elements[0];
}
return elements;
}
render() {
const { reduxState: _a, ...rest } = this.props;
const slotElements = this.getSlotElements();
const props = {
[propName]: slotElements,
...rest,
};
return <WrappedComponent {...props} />;
}
};
});
const mapStateToProps = state => ({
reduxState: state,
});
/**
* Exports a HOC that provides a property `slotElements`.
* @param {Object} [options] configuration
* @param {string|array} [options.slot] Slot name or Array of Slot Names
* @param {element|array} [options.defaultComponent] Default Components or Array of such
* @param {number|array} [options.size] Slot size or an Array of slot size
* @param {string} [options.passthroughPropName] The property to find the passthrough prop
* @param {string} [options.propName] New property name, defaults to `slotElements`
*
* @return {func} Returns a HOC that per default provides the property `slotElements` with an
* Array of Slot Elements or in case of multiple slots, an Array of Slot Element
* Arrays.
*
* Example:
* withSlotElements({
* slot: 'awesomeSlot',
* size: 1,
* })(MyComponent);
*/
export default settings => {
return compose(connect(mapStateToProps, null), createHOC(settings));
};
@@ -0,0 +1,27 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { Subscriber } from 'react-broadcast';
import get from 'lodash/get';
/**
* WithSubscribeToMore provides a property `subscribeToMore` to the wrapped component.
* Calling `subscribeToMore` is the same as calling `data.subscribeToMore` from the
* Apollo React API.
*/
export default hoistStatics(WrappedComponent => {
class WithSubscribeToMore extends React.Component {
render() {
return (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
subscribeToMore={get(data, 'subscribeToMoreThrottled')}
/>
)}
</Subscriber>
);
}
}
return WithSubscribeToMore;
});
@@ -0,0 +1,26 @@
import React from 'react';
import hoistStatics from 'recompose/hoistStatics';
import { Subscriber } from 'react-broadcast';
import get from 'lodash/get';
/**
* WithVariables provides a property `variables` to the wrapped component.
* These are the variables of the parent Query.
*/
export default hoistStatics(WrappedComponent => {
class WithVariables extends React.Component {
render() {
return (
<Subscriber channel="queryData">
{data => (
<WrappedComponent
{...this.props}
variables={get(data, 'variables')}
/>
)}
</Subscriber>
);
}
}
return WithVariables;
});
+98 -64
View File
@@ -7,6 +7,7 @@ import isEmpty from 'lodash/isEmpty';
import flatten from 'lodash/flatten';
import mapValues from 'lodash/mapValues';
import get from 'lodash/get';
import values from 'lodash/values';
import { getDisplayName } from 'coral-framework/helpers/hoc';
import camelize from '../helpers/camelize';
@@ -67,71 +68,102 @@ function addMetaDataToSlotComponents(plugins) {
});
}
// @Deprecated
const showPluginConfigDeprecationWarningOnce = (() => {
let shown = false;
return () => {
if (!shown) {
shown = true;
console.warn(
`deprecation warning: config.plugin_config will be phased out soon, please replace calls from config.plugin_config to config.plugins_config`
);
}
};
})();
/**
* getSlotComponentProps calculate the props we would pass to the slot component.
* query datas are only passed to the component if it is defined in `component.fragments`.
*/
function getSlotComponentProps(component, reduxState, props, queryData) {
// @Deprecated
const pluginsConfig =
get(reduxState, 'config.plugins_config') ||
get(reduxState, 'config.plugin_config') ||
emptyConfig;
if (
process.env.NODE_ENV !== 'production' &&
!!get(reduxState, 'config.plugin_config')
) {
showPluginConfigDeprecationWarningOnce();
}
console.log('slot plugins_config', get(reduxState, 'config.plugins_config'));
const debugProps = pluginsConfig.debug
? {
'data-slot-name': props.fill,
}
: {};
return {
...props,
...debugProps,
config: pluginsConfig,
...(component.fragments
? pick(queryData, Object.keys(component.fragments))
: withWarnings(component, queryData)),
};
}
/**
* splitProps detects objects coming from the query and
* returns `queryData` and `rest`. We use `__typename`
* in order to detect objects from the query.
*/
function splitProps(props) {
const rest = { ...props };
const queryData = {};
Object.keys(props).forEach(k => {
if (
get(props[k], `__typename`) ||
get(props[k], `0.__typename`) // Arrays
) {
queryData[k] = props[k];
delete rest[k];
}
});
return { queryData, rest };
}
class PluginsService {
constructor(plugins) {
this.plugins = plugins;
addMetaDataToSlotComponents(plugins);
}
isSlotEmpty(slot, reduxState, props = {}, queryData = {}) {
return (
this.getSlotElements(slot, reduxState, props, queryData).length === 0
);
isSlotEmpty(slot, reduxState, props = {}) {
return this.getSlotElements(slot, reduxState, props).length === 0;
}
/**
* getSlotComponentProps calculate the props we would pass to the slot component.
* query datas are only passed to the component if it is defined in `component.fragments`.
* Returns props that would pass to the given slot component.
*/
showPluginsConfigWarning = true;
getSlotComponentProps(component, reduxState, props, queryData) {
if (
process.env.NODE_ENV !== 'production' &&
!!get(reduxState, 'config.plugin_config') &&
this.showPluginsConfigWarning
) {
console.warn(
`deprecation warning: config.plugin_config will be phased out soon, please replace calls from config.plugin_config to config.plugins_config`
);
this.showPluginsConfigWarning = false;
}
console.log(
'slot plugins_config',
get(reduxState, 'config.plugins_config')
);
// @Deprecated plugin_config
const pluginsConfig =
merge(
get(reduxState, 'config.plugins_config'),
get(reduxState, 'config.plugin_config')
) || emptyConfig;
const debugProps = pluginsConfig.debug
? {
'data-slot-name': props.fill,
}
: {};
return {
...props,
...debugProps,
config: pluginsConfig,
...(component.fragments
? pick(queryData, Object.keys(component.fragments))
: withWarnings(component, queryData)),
};
getSlotComponentProps(component, reduxState, props) {
const { queryData, rest } = splitProps(props);
return getSlotComponentProps(component, reduxState, rest, queryData);
}
/**
* Returns React Elements for given slot.
*/
getSlotElements(slot, reduxState, props = {}, queryData = {}, options = {}) {
getSlotElements(slot, reduxState, props = {}, options = {}) {
const pluginsConfig =
get(reduxState, 'config.plugins_config') || emptyConfig;
const { slotSize = 0 } = options;
get(reduxState, 'config.plugins_config') ||
get(reduxState, 'config.plugin_config') ||
emptyConfig;
const { size = 0 } = options;
const { queryData, rest } = splitProps(props);
const isDisabled = component => {
if (
@@ -144,10 +176,10 @@ class PluginsService {
// Check if component is excluded.
if (component.isExcluded) {
let resolvedProps = this.getSlotComponentProps(
let resolvedProps = getSlotComponentProps(
component,
reduxState,
props,
rest,
queryData
);
if (component.mapStateToProps) {
@@ -168,15 +200,15 @@ class PluginsService {
.map(o => o.module.slots[slot])
);
if (slotSize > 0 && slots.length > slotSize) {
if (size > 0 && slots.length > size) {
console.warn(
`Slot[${slot}] supports a maximum of ${slotSize} plugins providing slots, got ${
`Slot[${slot}] supports a maximum of ${size} plugins providing slots, got ${
slots.length
}, will only use the first ${slotSize}`
}, will only use the first ${size}`
);
}
return (slotSize > 0 ? slots.slice(0, slotSize) : slots)
return (size > 0 ? slots.slice(0, size) : slots)
.map((component, i) => ({
component,
disabled: isDisabled(component),
@@ -186,12 +218,7 @@ class PluginsService {
.map(({ component, key }) =>
React.createElement(component, {
key,
...this.getSlotComponentProps(
component,
reduxState,
props,
queryData
),
...getSlotComponentProps(component, reduxState, rest, queryData),
})
);
}
@@ -217,9 +244,16 @@ class PluginsService {
}
getGraphQLExtensions() {
return this.plugins
.map(o => pick(o.module, ['mutations', 'queries', 'fragments']))
.filter(o => !isEmpty(o));
return flatten(
this.plugins.map(o => {
const extension = pick(o.module, ['mutations', 'queries', 'fragments']);
// Include extension defined in Slot Components.
const slotComponentExtensions = !o.module.slots
? []
: flatten(values(o.module.slots)).map(cmp => cmp.graphqlExtension);
return [extension, ...slotComponentExtensions];
})
).filter(o => !isEmpty(o));
}
getModQueueConfigs() {
+1 -1
View File
@@ -352,7 +352,7 @@ ar:
personal_info: "هذا التعليق يكشف عن معلومات تعريف شخصية"
post: نشر
profile: الملف الشخصي
profile_settings: "إعدادات الملف الشخصي"
profile_settings: "إعدادات"
reply: رد
report: أبلغ
report_notif: "شكرا على الإبلاغ عن هذا التعليق. تم إبلاغ فريق الإشراف لدينا وسيراجعه قريبًا."
+3 -3
View File
@@ -16,7 +16,7 @@ da:
send: "Send"
notify_ban_headline: "Underret brugeren om bannet"
notify_ban_description: "Dette meddeler brugeren via e-mail, at der er blevet bandlyst fra fællesskabet"
email_message_ban: "Kære {0},\n\nEn person med adgang til din konto har overtrådt vores retningslinjer for fællesskabet. Som følge heraf er din konto blevet forbudt. Du vil ikke længere kunne kommentere, like eller rapportere kommentarer. Hvis du mener at dette er sket ved en fejl, bedes du kontakte vores fællesskabsteam."
email_message_ban: "Kære {0},\n\nEn person med adgang til din konto har overtrådt vores retningslinjer for fællesskabet. Som følge heraf er din konto blevet forbudt. Du vil ikke længere kunne kommentere, like eller rapportere kommentarer. Hvis du mener at dette er sket ved en fejl, bedes du kontakte vores fællesskabsteam."
bio_offensive: "Denne biografi er stødende"
cancel: "Afbryd"
confirm_email:
@@ -256,7 +256,7 @@ da:
label: "Nyt brugernavn"
msg: "Din konto er midlertidigt suspenderet, fordi dit brugernavn er blevet anset for upassende. For at gendanne din konto skal du indtaste et nyt brugernavn. Kontakt os venligst, hvis du har spørgsmål."
changed_name:
msg: "Dit brugernavn ændring er nuværende under gennemgang af vores modereringsteam."
msg: "Dit brugernavn ændring er nuværende under gennemgang af vores modereringsteam."
my_comments: "Mine kommentarer"
my_profile: "Min profil"
new_count: "Se {0} mere {1}"
@@ -350,7 +350,7 @@ da:
personal_info: "Denne kommentar afslører personligt identificerbare oplysninger"
post: "Post"
profile: "Profil"
profile_settings: "Profilindstillinger"
profile_settings: "Indstillinger"
reply: "Svar"
report: "Rapporter"
report_notif: "Tak fordi du rapporterede denne kommentar. Vores modereringsteam har fået besked, og vil gennemgå det inden for kort tid."
+2 -2
View File
@@ -349,7 +349,7 @@ de:
personal_info: "Dieser Kommentar enthält zu viel personenbezogene Daten"
post: Post # Kontext?
profile: Profil
profile_settings: "Profil-Einstellungen"
profile_settings: "Einstellungen"
reply: Antworten
report: Melden
report_notif: "Vielen Dank für Ihre Meldung. Unsere Moderatoren wurden informiert und werden sich in Kürze darum kümmern."
@@ -397,7 +397,7 @@ de:
suspend_user: "Nutzer vorübergehend sperren"
email_message_suspend: "Sehr geehrte/r {0}, entsprechend der Community-Richtlinien von {1} wurde Ihr Konto vorübergehend gesperrt. Während der Sperrung können Sie weder kommentieren noch andere Aktionen ausführen. Nehmen Sie {2} wieder an der Diskussion teil."
title_notify: "Den Nutzer über die vorübergehende Kontosperrung informieren"
notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}."
notify_suspend_until: "Nutzer {0} wurde vorübergehend gesperrt. Diese Sperrung endet automatisch {1}."
description_notify: "Während der Sperrung hat der Nutzer keinen Zugriff auf das Konto."
write_message: "Nachricht verfassen"
send: Senden
+1 -1
View File
@@ -352,7 +352,7 @@ en:
personal_info: "This comment reveals personally identifiable information"
post: Post
profile: Profile
profile_settings: "Profile Settings"
profile_settings: "Settings"
reply: Reply
report: Report
report_notif: "Thank you for reporting this comment. Our moderation team has been notified and will review it shortly."
+1 -1
View File
@@ -351,7 +351,7 @@ es:
personal_info: "Este comentario muestra información personal"
post: Publicar
profile: Perfil
profile_settings: "Configuración del Perfil"
profile_settings: "Configuración"
reply: Responder
report: Reportar
report_notif: "Gracias por reportar este comentario. Nuestro equipo de moderación ha sido notificado y muy pronto lo va a revisar."
+1 -1
View File
@@ -351,7 +351,7 @@ fr:
personal_info: "Ce commentaire révèle des informations personnelles identifiables"
post: Publier
profile: Profil
profile_settings: "Paramètres du profil"
profile_settings: "Paramètres"
reply: Répondre
report: Signaler
report_notif: "Merci de signaler ce commentaire. Notre équipe de modération a é té informée."
+2 -2
View File
@@ -65,7 +65,7 @@ nl_NL:
notify_approved: '{0} heeft gebruikersnaam {1} goedgekeurd'
notify_rejected: '{0} heeft gebruikersnaam {1} afgekeurd'
notify_flagged: '{0} heeft gebruikersnaam {1} gerapporteerd'
notify_changed: 'gebruiker {0} heeft zijn/haar gebruikersnaam veranderd naar {1}'
notify_changed: 'gebruiker {0} heeft zijn/haar gebruikersnaam veranderd naar {1}'
community:
account_creation_date: "Aanmaakdatum account"
active: Actief
@@ -350,7 +350,7 @@ nl_NL:
personal_info: "Deze reactie onthult persoonlijk identificeerbare gegevens."
post: Plaats
profile: Profiel
profile_settings: "Profiel instellingen"
profile_settings: "Instellingen"
reply: Beantwoord
report: Rapporteer
report_notif: "Dank voor het rapporteren van deze reactie. Deze wordt zo snel mogelijk gemodereerd."
+1 -1
View File
@@ -349,7 +349,7 @@ pt_BR:
personal_info: "Este comentário revela informações de identificação pessoal"
post: Publicar
profile: Perfil
profile_settings: "Configurações de perfil"
profile_settings: "Configurações"
reply: Responder
report: Denunciar
report_notif: "Obrigado por denunciar este comentário. Nossa equipe de moderação foi notificada e irá revisá-la em breve."
+1 -1
View File
@@ -351,7 +351,7 @@ zh_CN:
personal_info: "这个评论透露了可确定个人身份的信息"
post: "发表"
profile: "资料"
profile_settings: "资料设定"
profile_settings: "设置"
reply: "回复"
report: "举报"
report_notif: "感谢您举报此评论。我们的审核小组已经收到通知,并会很快进行审查。"
+1 -1
View File
@@ -351,7 +351,7 @@ zh_TW:
personal_info: "該評論洩露了個人身份資訊"
post: 帖子
profile: 概況
profile_settings: "概況設置"
profile_settings: "設置"
reply: 回覆
report: 舉報
report_notif: "感謝您舉報此評論。我們的審核小組已經收到通知,並會很快進行審查。"
+1
View File
@@ -159,6 +159,7 @@
"query-string": "^5.0.0",
"react": "^15.4.2",
"react-apollo": "^1.4.12",
"react-broadcast": "^0.6.2",
"react-dom": "^15.4.2",
"react-input-autosize": "^1.1.4",
"react-mdl": "^1.11.0",
+6
View File
@@ -13,6 +13,11 @@ export {
withResendEmailConfirmation,
withSetUsername,
withEnumValues,
withVariables,
withFetchMore,
withSubscribeToMore,
withRefetch,
withGraphQLExtension,
} from 'coral-framework/hocs';
export {
withIgnoreUser,
@@ -21,3 +26,4 @@ export {
withStopIgnoringUser,
withSetCommentStatus,
} from 'coral-framework/graphql/mutations';
export { compose } from 'recompose';
@@ -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,
};

Some files were not shown because too many files have changed in this diff Show More