Merge branch 'master' into edit-comment

This commit is contained in:
Benjamin Goering
2017-05-11 10:42:41 -07:00
committed by GitHub
16 changed files with 127 additions and 97 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright 2016 Mozilla Foundation
Copyright 2017 Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,6 +1,6 @@
# Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk)
Talk is currently in Beta! [Read more about Talk here.](https://coralproject.net/products/talk.html)
Online comments are broken. Our open-source Talk tool rethinks how moderation, comment display, and conversation function, creating the opportunity for safer, smarter discussions around your work. [Read more about Talk here.](https://coralproject.net/products/talk.html)
Third party licenses are available via the `/client/3rdpartylicenses.txt`
endpoint when the server is running with built assets.
@@ -0,0 +1,6 @@
import {ADD_EXTERNAL_CONFIG} from '../constants/config';
export const addExternalConfig = config => ({
type: ADD_EXTERNAL_CONFIG,
config
});
@@ -7,4 +7,3 @@ export const setActiveTab = (tab) => (dispatch, getState) => {
dispatch(viewAllComments());
}
};
@@ -0,0 +1 @@
export const ADD_EXTERNAL_CONFIG = 'ADD_EXTERNAL_CONFIG';
@@ -9,23 +9,21 @@ import update from 'immutability-helper';
import {Spinner} from 'coral-ui';
import {authActions, assetActions, pym} from 'coral-framework';
import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils';
import Embed from '../components/Embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {setActiveTab} from '../actions/embed';
import Stream from './Stream';
import Embed from '../components/Embed';
import {setActiveTab} from '../actions/embed';
import {setCommentCountCache, viewAllComments} from '../actions/stream';
import {getDefinitionName, separateDataAndRoot} from 'coral-framework/utils';
const {logout, checkLogin} = authActions;
const {fetchAssetSuccess} = assetActions;
class EmbedContainer extends React.Component {
componentDidMount() {
pym.sendMessage('childReady');
}
componentWillReceiveProps(nextProps) {
if(this.props.root.me && !nextProps.root.me) {
if (this.props.root.me && !nextProps.root.me) {
// Refetch because on logout `excludeIgnored` becomes `false`.
// TODO: logout via mutation and obsolete this?
@@ -33,7 +31,7 @@ class EmbedContainer extends React.Component {
}
const {fetchAssetSuccess} = this.props;
if(!isEqual(nextProps.root.asset, this.props.root.asset)) {
if (!isEqual(nextProps.root.asset, this.props.root.asset)) {
// TODO: remove asset data from redux store.
fetchAssetSuccess(nextProps.root.asset);
@@ -48,7 +46,7 @@ class EmbedContainer extends React.Component {
}
componentDidUpdate(prevProps) {
if(!isEqual(prevProps.root.comment, this.props.root.comment)) {
if (!isEqual(prevProps.root.comment, this.props.root.comment)) {
// Scroll to a permalinked comment if one is in the URL once the page is done rendering.
setTimeout(() => pym.scrollParentToChildEl('coralStream'), 0);
@@ -89,7 +87,7 @@ export const withQuery = graphql(EMBED_QUERY, {
return reduceEditCommentActionsToUpdateStreamQuery(previousResult, action, variables);
},
}),
props: ({data}) => separateDataAndRoot(data),
props: ({data}) => separateDataAndRoot(data)
});
const mapStateToProps = state => ({
@@ -99,25 +97,26 @@ const mapStateToProps = state => ({
assetId: state.stream.assetId,
assetUrl: state.stream.assetUrl,
activeTab: state.embed.activeTab,
config: state.config
});
const mapDispatchToProps = dispatch =>
bindActionCreators({
fetchAssetSuccess,
checkLogin,
setCommentCountCache,
viewAllComments,
logout,
setActiveTab,
}, dispatch);
bindActionCreators(
{
logout,
checkLogin,
setActiveTab,
viewAllComments,
fetchAssetSuccess,
setCommentCountCache
},
dispatch
);
export default compose(
connect(mapStateToProps, mapDispatchToProps),
branch(
props => !props.auth.checkedInitialLogin,
renderComponent(Spinner),
),
withQuery,
branch(props => !props.auth.checkedInitialLogin && props.config, renderComponent(Spinner)),
withQuery
)(EmbedContainer);
/**
@@ -202,4 +201,3 @@ function reduceEditCommentActionsToUpdateStreamQuery(previousResult, action) {
});
return resultReflectingEdit;
}
+8
View File
@@ -4,10 +4,12 @@ import {ApolloProvider} from 'react-apollo';
import {client} from 'coral-framework/services/client';
import {checkLogin} from 'coral-framework/actions/auth';
import {addExternalConfig} from 'coral-embed-stream/src/actions/config';
import reducers from './reducers';
import localStore, {injectReducers} from 'coral-framework/services/store';
import AppRouter from './AppRouter';
import {pym} from 'coral-framework';
injectReducers(reducers);
@@ -16,6 +18,12 @@ const store = (window.opener && window.opener.coralStore) ? window.opener.coralS
// Don't run this in the popup.
if (store === localStore) {
store.dispatch(checkLogin());
pym.sendMessage('getConfig');
pym.onMessage('config', config => {
store.dispatch(addExternalConfig(JSON.parse(config)));
});
}
render(
@@ -0,0 +1,15 @@
import {ADD_EXTERNAL_CONFIG} from '../constants/config';
const initialState = {};
export default function config(state = initialState, action) {
switch (action.type) {
case ADD_EXTERNAL_CONFIG:
return {
...state,
...action.config
};
default:
return state;
}
}
@@ -1,7 +1,9 @@
import stream from './stream';
import embed from './embed';
import config from './config';
import stream from './stream';
export default {
stream,
embed,
stream,
config
};
+43 -40
View File
@@ -22,18 +22,18 @@ const snackbarStyles = {
transform: 'translate(-50%, 20px)',
bottom: 0,
boxSizing: 'border-box',
fontFamily: 'Helvetica, \'Helvetica Neue\', Verdana, sans-serif'
fontFamily: 'Helvetica, "Helvetica Neue", Verdana, sans-serif'
};
// This function should return value of window.Coral
const Coral = {};
const Talk = Coral.Talk = {};
const Talk = (Coral.Talk = {});
// build the URL to load in the pym iframe
function buildStreamIframeUrl(talkBaseUrl, query) {
let url = [
talkBaseUrl,
(talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/'
talkBaseUrl.match(/\/$/) ? '' : '/', // make sure no double-'/' if opts.talk already ends with '/'
'embed/stream?'
].join('');
@@ -44,11 +44,21 @@ function buildStreamIframeUrl(talkBaseUrl, query) {
// Set up postMessage listeners/handlers on the pymParent
// e.g. to resize the iframe, and navigate the host page
function configurePymParent(pymParent) {
function configurePymParent(pymParent, opts) {
let notificationOffset = 200;
let ready = false;
let cachedHeight;
const snackbar = document.createElement('div');
// Sends config to pymChild
function sendConfig(config) {
pymParent.sendMessage('config', JSON.stringify(config));
}
// Sends config to the child
pymParent.onMessage('getConfig', function() {
sendConfig(opts || {});
});
snackbar.id = 'coral-notif';
for (let key in snackbarStyles) {
@@ -69,12 +79,12 @@ function configurePymParent(pymParent) {
}
});
pymParent.onMessage('coral-clear-notification', function () {
pymParent.onMessage('coral-clear-notification', function() {
snackbar.style.opacity = 0;
});
// remove the permalink comment id from the hash
pymParent.onMessage('coral-view-all-comments', function () {
pymParent.onMessage('coral-view-all-comments', function() {
window.history.replaceState(
{},
document.title,
@@ -82,7 +92,7 @@ function configurePymParent(pymParent) {
);
});
pymParent.onMessage('coral-alert', function (message) {
pymParent.onMessage('coral-alert', function(message) {
const [type, text] = message.split('|');
snackbar.style.transform = 'translate(-50%, 20px)';
snackbar.style.opacity = 0;
@@ -110,39 +120,21 @@ function configurePymParent(pymParent) {
pymParent.sendMessage('position', position);
});
// Tell child when parent's DOMContentLoaded
pymParent.onMessage('childReady', function () {
const interval = setInterval(function () {
if (ready) {
window.clearInterval(interval);
// TODO: It's weird to me that this is sent here
pymParent.sendMessage('DOMContentLoaded');
}
}, 100);
});
// When end-user clicks link in iframe, open it in parent context
pymParent.onMessage('navigate', function (url) {
pymParent.onMessage('navigate', function(url) {
window.open(url, '_blank').focus();
});
// wait till images and other iframes are loaded before scrolling the page.
// or do we want to be more aggressive and scroll when we hit DOM ready?
document.addEventListener('DOMContentLoaded', function () {
ready = true;
});
// get dimensions of viewport
const viewport = () => {
let e = window, a = 'inner';
if ( !( 'innerWidth' in window ) ){
if (!('innerWidth' in window)) {
a = 'client';
e = document.documentElement || document.body;
}
return {
width : e[`${a}Width`],
height : e[`${a}Height`]
width: e[`${a}Width`],
height: e[`${a}Height`]
};
};
}
@@ -156,18 +148,24 @@ function configurePymParent(pymParent) {
* @param {String} [opts.asset_url] - Asset URL
* @param {String} [opts.asset_id] - Asset ID
*/
Talk.render = function (el, opts) {
Talk.render = function(el, opts) {
if (!el) {
throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.');
throw new Error(
'Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'
);
}
if (typeof el !== 'object') {
throw new Error(`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`);
throw new Error(
`Coral.Talk.render() expected HTMLElement but got ${el} (${typeof el})`
);
}
opts = opts || {};
// TODO: infer this URL without explicit user input (if possible, may have to be added at build/render time of this script)
if (!opts.talk) {
throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL');
throw new Error(
'Coral.Talk.render() expects opts.talk as the Talk Base URL'
);
}
// Ensure el has an id, as pym can't directly accept the HTMLElement.
@@ -186,16 +184,21 @@ Talk.render = function (el, opts) {
try {
query.asset_url = document.querySelector('link[rel="canonical"]').href;
} catch (e) {
console.warn('This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.');
console.warn(
'This page does not include a canonical link tag. Talk has inferred this asset_url from the window object. Query params have been stripped, which may cause a single thread to be present across multiple pages.'
);
query.asset_url = window.location.origin + window.location.pathname;
}
}
configurePymParent(new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
}));
configurePymParent(
new pym.Parent(el.id, buildStreamIframeUrl(opts.talk, query), {
title: opts.title,
id: `${el.id}_iframe`,
name: `${el.id}_iframe`
}),
opts
);
};
export default Coral;
@@ -1,3 +1,7 @@
.inline {
display: inline-block;
}
.debug {
background-color: coral;
}
+9 -3
View File
@@ -1,12 +1,13 @@
import React from 'react';
import cn from 'classnames';
import styles from './Slot.css';
import {connect} from 'react-redux';
import {getSlotElements} from 'coral-framework/helpers/plugins';
export default function Slot ({fill, inline = false, ...rest}) {
function Slot ({fill, inline = false, plugin_config: config, ...rest}) {
return (
<div className={cn({[styles.inline]: inline})}>
{getSlotElements(fill, rest)}
<div className={cn({[styles.inline]: inline, [styles.debug]: config.debug})}>
{getSlotElements(fill, {...rest, config})}
</div>
);
}
@@ -14,3 +15,8 @@ export default function Slot ({fill, inline = false, ...rest}) {
Slot.propTypes = {
fill: React.PropTypes.string
};
const mapStateToProps = ({config: {plugin_config = {}}}) => ({plugin_config});
export default connect(mapStateToProps, null)(Slot);
@@ -8,4 +8,3 @@ export const UPDATE_ASSET_SETTINGS_FAILURE = 'UPDATE_ASSET_SETTINGS_FAILURE';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
@@ -1,9 +0,0 @@
export const UPDATE_CONFIG_REQUEST = 'UPDATE_CONFIG_REQUEST';
export const UPDATE_CONFIG_SUCCESS = 'UPDATE_CONFIG_SUCCESS';
export const UPDATE_CONFIG_FAILURE = 'UPDATE_CONFIG_FAILURE';
export const UPDATE_CONFIG = 'UPDATE_CONFIG';
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
export const ADD_ITEM = 'ADD_ITEM';
+6 -12
View File
@@ -15,8 +15,7 @@ class FlagButton extends Component {
message: '',
step: 0,
posted: false,
localPost: null,
localDelete: false
localPost: null
}
componentDidUpdate () {
@@ -27,17 +26,12 @@ class FlagButton extends Component {
// When the "report" button is clicked expand the menu
onReportClick = () => {
const {currentUser, deleteAction, flaggedByCurrentUser, flag} = this.props;
const {localPost, localDelete} = this.state;
const localFlagged = (flaggedByCurrentUser && !localDelete) || localPost;
const {currentUser} = this.props;
if (!currentUser) {
this.props.showSignInDialog();
return;
}
if (localFlagged) {
this.setState((prev) => prev.localPost ? {...prev, localPost: null, step: 0} : {...prev, localDelete: true});
deleteAction(localPost || flag.current_user.id);
} else if (this.state.showMenu){
if (this.state.showMenu) {
this.closeMenu();
} else {
this.setState({showMenu: true});
@@ -136,14 +130,14 @@ class FlagButton extends Component {
render () {
const {getPopupMenu, flaggedByCurrentUser} = this.props;
const {localPost, localDelete} = this.state;
const flagged = (flaggedByCurrentUser && !localDelete) || localPost;
const {localPost} = this.state;
const flagged = flaggedByCurrentUser || localPost;
const popupMenu = getPopupMenu[this.state.step](this.state.itemType);
return <div className={`${name}-container`}>
<button
ref={ref => this.flagButton = ref}
onClick={!this.props.banned ? this.onReportClick : null}
onClick={!this.props.banned && !flaggedByCurrentUser && !localPost ? this.onReportClick : null}
className={`${name}-button`}>
{
flagged
+5 -1
View File
@@ -28,7 +28,11 @@
Coral.Talk.render(document.getElementById('coralStreamEmbed'), {
talk: '/',
asset_url: '<%= asset_url ? asset_url : '' %>',
asset_id: '<%= asset_id ? asset_id : '' %>'
asset_id: '<%= asset_id ? asset_id : '' %>',
plugin_config: {
test: 'data',
debug: false
}
})
"></script>
</main>