diff --git a/client/blocks/reader-full-post/index.jsx b/client/blocks/reader-full-post/index.jsx index 2123abc514b98..a52f13cf963f8 100644 --- a/client/blocks/reader-full-post/index.jsx +++ b/client/blocks/reader-full-post/index.jsx @@ -6,7 +6,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { translate } from 'i18n-calypso'; import classNames from 'classnames'; -import { get, startsWith, pickBy } from 'lodash'; +import { get, startsWith } from 'lodash'; import config from 'config'; /** @@ -62,6 +62,8 @@ import { getPostByKey } from 'state/reader/posts/selectors'; import isLikedPost from 'state/selectors/is-liked-post'; import QueryPostLikes from 'components/data/query-post-likes'; import getCurrentStream from 'state/selectors/get-reader-current-stream'; +import { getReaderFullViewPostKey } from 'state/reader/full-view/selectors/get-reader-full-view-post-key'; +import { setReaderFullViewPostKey } from 'state/reader/full-view/actions'; import { getNextItem, getPreviousItem } from 'state/reader/streams/selectors'; /** @@ -124,6 +126,7 @@ export class FullPostView extends React.Component { } componentWillUnmount() { + this.props.setReaderFullViewPostKey( null ); KeyboardShortcuts.off( 'close-full-post', this.handleBack ); KeyboardShortcuts.off( 'like-selection', this.handleLike ); KeyboardShortcuts.off( 'move-selection-down', this.goToNextPost ); @@ -477,8 +480,7 @@ export class FullPostView extends React.Component { export default connect( ( state, ownProps ) => { - const { feedId, blogId, postId } = ownProps; - const postKey = pickBy( { feedId: +feedId, blogId: +blogId, postId: +postId } ); + const postKey = getReaderFullViewPostKey( state ); const post = getPostByKey( state, postKey ) || { _state: 'pending' }; const { site_ID: siteId, is_external: isExternal } = post; @@ -492,8 +494,8 @@ export default connect( if ( ! isExternal && siteId ) { props.site = getSite( state, siteId ); } - if ( feedId ) { - props.feed = getFeed( state, feedId ); + if ( ownProps.feedId ) { + props.feed = getFeed( state, ownProps.feedId ); } if ( ownProps.referral ) { props.referralPost = getPostByKey( state, ownProps.referral ); @@ -507,5 +509,5 @@ export default connect( return props; }, - { markPostSeen, likePost, unlikePost } + { markPostSeen, setReaderFullViewPostKey, likePost, unlikePost } )( FullPostView ); diff --git a/client/package.json b/client/package.json index b8679331af0c7..920e6f753e4ec 100644 --- a/client/package.json +++ b/client/package.json @@ -118,6 +118,7 @@ "page": "1.11.5", "path-browserify": "1.0.0", "percentage-regex": "3.0.0", + "phoenix": "1.4.16", "phone": "2.4.2", "photon": "file:../packages/photon", "prismjs": "1.17.1", diff --git a/client/reader/full-post/controller.js b/client/reader/full-post/controller.js index a04b4983a0a21..0a201983caf85 100644 --- a/client/reader/full-post/controller.js +++ b/client/reader/full-post/controller.js @@ -8,6 +8,7 @@ import { defer } from 'lodash'; /** * Internal Dependencies */ +import { setReaderFullViewPostKey } from 'state/reader/full-view/actions'; import { trackPageLoad } from 'reader/controller-helper'; import AsyncLoad from 'components/async-load'; @@ -26,6 +27,8 @@ export function blogPost( context, next ) { basePath = '/read/blogs/:blog_id/posts/:post_id', fullPageTitle = analyticsPageTitle + ' > Blog Post > ' + blogId + ' > ' + postId; + context.store.dispatch( setReaderFullViewPostKey( { blogId, postId, feedId: 0 } ) ); + let referral; if ( context.query.ref_blog && context.query.ref_post ) { referral = { blogId: context.query.ref_blog, postId: context.query.ref_post }; @@ -54,6 +57,8 @@ export function feedPost( context, next ) { basePath = '/read/feeds/:feed_id/posts/:feed_item_id', fullPageTitle = analyticsPageTitle + ' > Feed Post > ' + feedId + ' > ' + postId; + context.store.dispatch( setReaderFullViewPostKey( { blogId: 0, postId, feedId } ) ); + trackPageLoad( basePath, fullPageTitle, 'full_post' ); function closer() { diff --git a/client/state/action-types.js b/client/state/action-types.js index 1425e7df9ae47..bfb491d462718 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -496,6 +496,8 @@ export const KEYRING_SERVICES_RECEIVE = 'KEYRING_SERVICES_RECEIVE'; export const KEYRING_SERVICES_REQUEST = 'KEYRING_SERVICES_REQUEST'; export const KEYRING_SERVICES_REQUEST_FAILURE = 'KEYRING_SERVICES_REQUEST_FAILURE'; export const KEYRING_SERVICES_REQUEST_SUCCESS = 'KEYRING_SERVICES_REQUEST_SUCCESS'; +export const LASAGNA_SOCKET_CONNECTED = 'LASAGNA_SOCKET_CONNECTED'; +export const LASAGNA_SOCKET_DISCONNECTED = 'LASAGNA_SOCKET_DISCONNECTED'; export const LAYOUT_FOCUS_SET = 'LAYOUT_FOCUS_SET'; export const LAYOUT_NEXT_FOCUS_ACTIVATE = 'LAYOUT_NEXT_FOCUS_ACTIVATE'; export const LAYOUT_NEXT_FOCUS_SET = 'LAYOUT_NEXT_FOCUS_SET'; diff --git a/client/state/index.js b/client/state/index.js index 2d44a3224bd0e..28cdfca67dddd 100644 --- a/client/state/index.js +++ b/client/state/index.js @@ -7,6 +7,7 @@ import { createStore, applyMiddleware, compose } from 'redux'; /** * Internal dependencies */ +import config from 'config'; import initialReducer from './reducer'; /** @@ -67,6 +68,7 @@ export function createReduxStore( initialState, reducer = initialReducer ) { noticesMiddleware, isBrowser && require( './happychat/middleware.js' ).default, isBrowser && require( './happychat/middleware-calypso.js' ).default, + isBrowser && config.isEnabled( 'lasagna' ) && require( './lasagna/middleware.js' ).default, isBrowser && require( './analytics/middleware.js' ).analyticsMiddleware, isBrowser && require( './lib/middleware.js' ).default, isAudioSupported && require( './audio/middleware.js' ).default, diff --git a/client/state/lasagna/actions.js b/client/state/lasagna/actions.js new file mode 100644 index 0000000000000..6a4f243f1c50f --- /dev/null +++ b/client/state/lasagna/actions.js @@ -0,0 +1,2 @@ +export const socketConnected = () => ( { type: 'LASAGNA_SOCKET_CONNECTED' } ); +export const socketDisconnected = () => ( { type: 'LASAGNA_SOCKET_DISCONNECTED' } ); diff --git a/client/state/lasagna/middleware.js b/client/state/lasagna/middleware.js new file mode 100644 index 0000000000000..4e1a424010b92 --- /dev/null +++ b/client/state/lasagna/middleware.js @@ -0,0 +1,67 @@ +/** + * Internal dependencies + */ +import wpcom from 'lib/wp'; +import { getCurrentUser } from 'state/current-user/selectors'; +import { socket, socketConnect, socketDisconnect } from './socket'; +import privatePostChannelMiddleware from './private-post-channel/actions-to-events'; +import publicPostChannelMiddleware from './public-post-channel/actions-to-events'; +import userChannelMiddleware from './user-channel/actions-to-events'; + +let socketConnecting = false; + +/** + * Compose a list of middleware into one middleware + * Props @rhc3 + * + * @param m middlewares to compose + */ +const combineMiddleware = ( ...m ) => { + return store => { + const initialized = m.map( middleware => middleware( store ) ); + return next => initialized.reduce( ( chain, mw ) => mw( chain ), next ); + }; +}; + +/** + * Connection management middleware + * + * @param store middleware store + */ +const connectMiddleware = store => next => action => { + // bail unless this is a section set with the section definition + if ( action.type !== 'SECTION_SET' || ! action.section ) { + return next( action ); + } + + // connect if we are going to the reader without a socket + if ( ! socket && ! socketConnecting && action.section.name === 'reader' ) { + socketConnecting = true; + const user = getCurrentUser( store.getState() ); + + wpcom + .request( { + method: 'POST', + path: '/jwt/sign', + body: { payload: JSON.stringify( { user } ) }, + } ) + .then( ( { jwt } ) => { + socketConnect( store, jwt, user.ID ); + socketConnecting = false; + } ); + } + + // disconnect if we are leaving the reader with a socket + else if ( socket && action.section.name !== 'reader' ) { + socketDisconnect( store ); + } + + return next( action ); +}; + +export default combineMiddleware( + connectMiddleware, + userChannelMiddleware, + privatePostChannelMiddleware, + publicPostChannelMiddleware +); diff --git a/client/state/lasagna/private-post-channel/actions-to-events.js b/client/state/lasagna/private-post-channel/actions-to-events.js new file mode 100644 index 0000000000000..782227041c81f --- /dev/null +++ b/client/state/lasagna/private-post-channel/actions-to-events.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; + +/** + * Internal Dependencies + */ +import { LASAGNA_SOCKET_CONNECTED, ROUTE_SET } from 'state/action-types'; +import { READER_POST_SEEN } from 'state/reader/action-types'; +import { getReaderFullViewPostKey } from 'state/reader/full-view/selectors/get-reader-full-view-post-key'; +import { getPostByKey } from 'state/reader/posts/selectors'; +import { getSite } from 'state/reader/sites/selectors'; +import registerEventHandlers from './events-to-actions'; +import { socket } from '../socket'; + +let channel = null; + +const channelTopicPrefix = 'private:push:wp_post:'; +const debug = debugFactory( 'lasagna:channel:private:push:wp_post' ); + +const joinChannel = ( store, site, post, postKey ) => { + if ( ! socket || ! site.is_private ) { + return; + } + + channel = socket.channel( channelTopicPrefix + post.global_ID, { post_key: postKey } ); + registerEventHandlers( channel, store ); + + channel + .join() + .receive( 'ok', () => debug( 'channel join ok' ) ) + .receive( 'error', ( { reason } ) => { + debug( 'channel join error', reason ); + channel.leave(); + channel = null; + } ); +}; + +const leaveChannel = () => { + channel && channel.leave(); + channel = null; +}; + +export default store => next => action => { + switch ( action.type ) { + case LASAGNA_SOCKET_CONNECTED: { + const state = store.getState(); + const postKey = getReaderFullViewPostKey( state ); + const post = getPostByKey( state, postKey ); + + if ( ! post ) { + break; + } + + const site = getSite( state, post.site_ID ); + + if ( ! site ) { + break; + } + + joinChannel( store, site, post, postKey ); + break; + } + + case READER_POST_SEEN: { + const postKey = getReaderFullViewPostKey( store.getState() ); + joinChannel( store, action.payload.site, action.payload.post, postKey ); + break; + } + + case ROUTE_SET: + leaveChannel(); + break; + } + + return next( action ); +}; diff --git a/client/state/lasagna/private-post-channel/events-to-actions.js b/client/state/lasagna/private-post-channel/events-to-actions.js new file mode 100644 index 0000000000000..2179e72e0a731 --- /dev/null +++ b/client/state/lasagna/private-post-channel/events-to-actions.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; + +/** + * Internal dependencies + */ +import { receiveComments } from 'state/comments/actions'; + +const debug = debugFactory( 'lasagna:channel:private:push:wp_post' ); + +export default function( channel, store ) { + channel.on( 'new_comment', ( { payload: comment } ) => { + debug( 'New comment', comment ); + + if ( ! comment ) { + return; + } + + store.dispatch( + receiveComments( { + siteId: comment.post.site_ID, + postId: comment.post.ID, + comments: [ comment ], + commentById: true, + } ) + ); + } ); +} diff --git a/client/state/lasagna/public-post-channel/actions-to-events.js b/client/state/lasagna/public-post-channel/actions-to-events.js new file mode 100644 index 0000000000000..a2e2766c1b6ff --- /dev/null +++ b/client/state/lasagna/public-post-channel/actions-to-events.js @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; + +/** + * Internal Dependencies + */ +import { LASAGNA_SOCKET_CONNECTED, ROUTE_SET } from 'state/action-types'; +import { READER_POST_SEEN } from 'state/reader/action-types'; +import { getPostByKey } from 'state/reader/posts/selectors'; +import { getReaderFullViewPostKey } from 'state/reader/full-view/selectors/get-reader-full-view-post-key'; +import { getSite } from 'state/reader/sites/selectors'; +import registerEventHandlers from './events-to-actions'; +import { socket } from '../socket'; + +let channel = null; + +const channelTopicPrefix = 'public:push:wp_post:'; +const debug = debugFactory( 'lasagna:channel:public:push:wp_post' ); + +const joinChannel = ( store, site, post ) => { + if ( ! socket || site.is_private ) { + return; + } + + channel = socket.channel( channelTopicPrefix + post.global_ID ); + registerEventHandlers( channel, store ); + + channel + .join() + .receive( 'ok', () => debug( 'channel join ok' ) ) + .receive( 'error', ( { reason } ) => { + debug( 'channel join error', reason ); + channel.leave(); + channel = null; + } ); +}; + +const leaveChannel = () => { + channel && channel.leave(); + channel = null; +}; + +export default store => next => action => { + switch ( action.type ) { + case LASAGNA_SOCKET_CONNECTED: { + const state = store.getState(); + const postKey = getReaderFullViewPostKey( state ); + const post = getPostByKey( state, postKey ); + + if ( ! post ) { + break; + } + + const site = getSite( state, post.site_ID ); + + if ( ! site ) { + break; + } + + joinChannel( store, site, post ); + break; + } + + case READER_POST_SEEN: + joinChannel( store, action.payload.site, action.payload.post ); + break; + + case ROUTE_SET: + leaveChannel(); + break; + } + + return next( action ); +}; diff --git a/client/state/lasagna/public-post-channel/events-to-actions.js b/client/state/lasagna/public-post-channel/events-to-actions.js new file mode 100644 index 0000000000000..887af882d9abe --- /dev/null +++ b/client/state/lasagna/public-post-channel/events-to-actions.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; + +/** + * Internal dependencies + */ +import { receiveComments } from 'state/comments/actions'; + +const debug = debugFactory( 'lasagna:channel:public:push:wp_post' ); + +export default function( channel, store ) { + channel.on( 'new_comment', ( { payload: comment } ) => { + debug( 'New comment', comment ); + + if ( ! comment ) { + return; + } + + store.dispatch( + receiveComments( { + siteId: comment.post.site_ID, + postId: comment.post.ID, + comments: [ comment ], + commentById: true, + } ) + ); + } ); +} diff --git a/client/state/lasagna/socket.js b/client/state/lasagna/socket.js new file mode 100644 index 0000000000000..cfbaa4eec6936 --- /dev/null +++ b/client/state/lasagna/socket.js @@ -0,0 +1,51 @@ +/** + * External Dependencies + */ +import createDebug from 'debug'; + +/** + * Internal dependencies + */ +import config from 'config'; +import { socketConnected, socketDisconnected } from 'state/lasagna/actions'; + +/** + * Module vars + */ +export let socket = null; + +const debug = createDebug( 'lasagna:socket' ); +const url = config( 'lasagna_url' ); + +export const socketConnect = ( store, jwt, userId ) => { + if ( socket !== null ) { + return; + } + + // import( /* webpackChunkName: "phoenix" */ 'phoenix' ).then( ( { Socket } ) => { + // socket = new Socket( url, { params: { jwt, user_id: userId } } ); + // + // socket.onOpen( () => { + // debug( 'socket opened' ); + // store.dispatch( socketConnected() ); + // } ); + // + // socket.onClose( () => { + // debug( 'socket closed' ); + // // @TODO: verify this Phoenix.js state, dispatch attempting reconnect here? + // } ); + // + // socket.onError( () => { + // debug( 'socket error' ); + // // @TODO: verify this Phoenix.js state, dispatch attempting reconnect here? + // } ); + // + // socket.connect(); + // } ); +}; + +export const socketDisconnect = store => { + socket && socket.disconnect(); + socket = null; + store.dispatch( socketDisconnected() ); +}; diff --git a/client/state/lasagna/user-channel/actions-to-events.js b/client/state/lasagna/user-channel/actions-to-events.js new file mode 100644 index 0000000000000..0761c9579a830 --- /dev/null +++ b/client/state/lasagna/user-channel/actions-to-events.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import debugFactory from 'debug'; + +/** + * Internal Dependencies + */ +import { LASAGNA_SOCKET_CONNECTED, LASAGNA_SOCKET_DISCONNECTED } from 'state/action-types'; +import { getCurrentUserId } from 'state/current-user/selectors'; +import { socket } from '../socket'; + +let channel = null; + +const debug = debugFactory( 'lasagna:channel:user:wpcom' ); + +const joinChannel = store => { + if ( ! socket || channel ) { + return; + } + + const userId = getCurrentUserId( store.getState() ); + + if ( ! userId ) { + return; + } + + channel = socket.channel( `user:wpcom:${ userId }` ); + // registerEventHandlers here + + channel + .join() + .receive( 'ok', () => debug( 'channel join ok' ) ) + .receive( 'error', ( { reason } ) => { + debug( 'channel join error', reason ); + channel.leave(); + channel = null; + } ); +}; + +const leaveChannel = () => { + channel && channel.leave(); + channel = null; +}; + +export default store => next => action => { + switch ( action.type ) { + case LASAGNA_SOCKET_CONNECTED: { + joinChannel( store ); + break; + } + + case LASAGNA_SOCKET_DISCONNECTED: + leaveChannel(); + break; + } + + return next( action ); +}; diff --git a/client/state/reader/action-types.js b/client/state/reader/action-types.js index 55c3464685091..7f51e84bc7980 100644 --- a/client/state/reader/action-types.js +++ b/client/state/reader/action-types.js @@ -33,8 +33,7 @@ export const READER_FOLLOWS_SYNC_START = 'READER_FOLLOWS_SYNC_START'; export const READER_FOLLOWS_SYNC_COMPLETE = 'READER_FOLLOWS_SYNC_COMPLETE'; export const READER_FOLLOW_TAG_RECEIVE = 'READER_FOLLOW_TAG_RECEIVE'; export const READER_FOLLOW_TAG_REQUEST = 'READER_FOLLOW_TAG_REQUEST'; -export const READER_FULLPOST_HIDE = 'READER_FULLPOST_HIDE'; -export const READER_FULLPOST_SHOW = 'READER_FULLPOST_SHOW'; +export const READER_FULL_VIEW_POST_KEY_SET = 'READER_FULL_VIEW_POST_KEY_SET'; export const READER_LISTS_FOLLOW = 'READER_LISTS_FOLLOW'; export const READER_LISTS_FOLLOW_FAILURE = 'READER_LISTS_FOLLOW_FAILURE'; export const READER_LISTS_FOLLOW_SUCCESS = 'READER_LISTS_FOLLOW_SUCCESS'; diff --git a/client/state/reader/full-view/actions.js b/client/state/reader/full-view/actions.js new file mode 100644 index 0000000000000..fb09da49a0515 --- /dev/null +++ b/client/state/reader/full-view/actions.js @@ -0,0 +1,9 @@ +/** + * Internal dependencies + */ +import { READER_FULL_VIEW_POST_KEY_SET } from 'state/reader/action-types'; + +export const setReaderFullViewPostKey = postKey => ( { + type: READER_FULL_VIEW_POST_KEY_SET, + postKey, +} ); diff --git a/client/state/reader/full-view/reducer.js b/client/state/reader/full-view/reducer.js new file mode 100644 index 0000000000000..3429deccd0406 --- /dev/null +++ b/client/state/reader/full-view/reducer.js @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { READER_FULL_VIEW_POST_KEY_SET, SERIALIZE } from 'state/reader/action-types'; +import { combineReducers } from 'state/utils'; + +/** + * Tracks the post key of the currently full viewed post + * + * @param {object} state Current state + * @param {object} action Action payload + * @returns {object} Updated state + */ +export function fullViewPostKey( state = null, action ) { + switch ( action.type ) { + case READER_FULL_VIEW_POST_KEY_SET: + return action.postKey; + + case SERIALIZE: + return null; + } + return state; +} + +export default combineReducers( { + fullViewPostKey, +} ); diff --git a/client/state/reader/full-view/selectors/get-reader-full-view-post-key.js b/client/state/reader/full-view/selectors/get-reader-full-view-post-key.js new file mode 100644 index 0000000000000..85b833331e890 --- /dev/null +++ b/client/state/reader/full-view/selectors/get-reader-full-view-post-key.js @@ -0,0 +1,3 @@ +export const getReaderFullViewPostKey = state => { + return state.reader.fullView.fullViewPostKey; +}; diff --git a/client/state/reader/reducer.js b/client/state/reader/reducer.js index b26294ed63ae7..8fb24c7afa3be 100644 --- a/client/state/reader/reducer.js +++ b/client/state/reader/reducer.js @@ -4,6 +4,7 @@ import { combineReducers, withStorageKey } from 'state/utils'; import conversations from './conversations/reducer'; +import fullView from './full-view/reducer'; import feeds from './feeds/reducer'; import feedSearches from './feed-searches/reducer'; import follows from './follows/reducer'; @@ -24,6 +25,7 @@ const combinedReducer = combineReducers( { feeds, feedSearches, follows, + fullView, lists, posts, recommendedSites, diff --git a/config/_shared.json b/config/_shared.json index a7fff18107d6c..97460fbb035ec 100644 --- a/config/_shared.json +++ b/config/_shared.json @@ -22,6 +22,7 @@ "hotjar_enabled": false, "hostname": false, "i18n_default_locale_slug": "en", + "lasagna_url": "wss://lasagna.pub/socket", "login_url": false, "logout_url": false, "mc_analytics_enabled": false, diff --git a/config/client.json b/config/client.json index 176c37bc66b58..60155c3fe18cb 100644 --- a/config/client.json +++ b/config/client.json @@ -17,6 +17,7 @@ "hostname", "i18n_default_locale_slug", "languages", + "lasagna_url", "login_url", "logout_url", "hotjar_enabled", diff --git a/config/desktop-development.json b/config/desktop-development.json index 333e8a5b9d5c2..ecdc8a271dc11 100644 --- a/config/desktop-development.json +++ b/config/desktop-development.json @@ -51,6 +51,7 @@ "jetpack/happychat": true, "jetpack_core_inline_update": true, "jitms": false, + "lasagna": false, "keyboard-shortcuts": true, "legal-updates-banner": true, "login/magic-login": false, diff --git a/config/desktop.json b/config/desktop.json index 74301504f9775..86ccdd8fbd43d 100644 --- a/config/desktop.json +++ b/config/desktop.json @@ -41,6 +41,7 @@ "jetpack/connect/remote-install": true, "jetpack/personalPlan": true, "jitms": false, + "lasagna": false, "legal-updates-banner": false, "login/magic-login": false, "login/native-login-links": false, diff --git a/config/development.json b/config/development.json index a7279c7c08d56..f3eb7e5873a9a 100644 --- a/config/development.json +++ b/config/development.json @@ -82,6 +82,7 @@ "jetpack/scan-product": true, "jitms": true, "keyboard-shortcuts": true, + "lasagna": true, "legal-updates-banner": true, "login/magic-login": true, "login/native-login-links": true, @@ -135,7 +136,7 @@ "publicize-preview": true, "push-notifications": true, "reader": true, - "reader/comment-polling": true, + "reader/comment-polling": false, "reader/full-errors": true, "reader/user-mention-suggestions": true, "recommend-plugins": true, diff --git a/config/horizon.json b/config/horizon.json index cf0e4130d0bde..32f99e2690af7 100644 --- a/config/horizon.json +++ b/config/horizon.json @@ -50,6 +50,7 @@ "jetpack/search-product": true, "jitms": true, "keyboard-shortcuts": true, + "lasagna": false, "legal-updates-banner": false, "login/native-login-links": true, "login/wp-login": true, diff --git a/config/production.json b/config/production.json index 4bd0c47205bf9..09065306e0011 100644 --- a/config/production.json +++ b/config/production.json @@ -51,6 +51,7 @@ "jetpack/happychat": true, "jetpack/search-product": true, "jitms": true, + "lasagna": false, "legal-updates-banner": false, "login/magic-login": true, "login/native-login-links": true, diff --git a/config/stage.json b/config/stage.json index 3f74377ca436c..2b27b75a85516 100644 --- a/config/stage.json +++ b/config/stage.json @@ -58,6 +58,7 @@ "jetpack/happychat": true, "jetpack/search-product": true, "jitms": true, + "lasagna": false, "legal-updates-banner": false, "login/magic-login": true, "login/native-login-links": true, diff --git a/config/test.json b/config/test.json index 46657ba384d44..ba51e5f0bba4b 100644 --- a/config/test.json +++ b/config/test.json @@ -44,6 +44,7 @@ "jetpack/connect/remote-install": true, "jetpack/happychat": true, "jitms": true, + "lasagna": false, "legal-updates-banner": true, "login/native-login-links": true, "login/wp-login": true, diff --git a/config/wpcalypso.json b/config/wpcalypso.json index f334dec1be67b..22a3bb884ea7d 100644 --- a/config/wpcalypso.json +++ b/config/wpcalypso.json @@ -65,6 +65,7 @@ "jetpack/happychat": true, "jetpack/search-product": true, "jitms": true, + "lasagna": true, "legal-updates-banner": false, "login/magic-login": true, "login/native-login-links": true, diff --git a/package-lock.json b/package-lock.json index 9a379bf4a1559..e4781a36277fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31774,6 +31774,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "phoenix": { + "version": "1.4.16", + "resolved": "https://registry.npmjs.org/phoenix/-/phoenix-1.4.16.tgz", + "integrity": "sha512-XmKVTlFQuRwTYBO1WU1OaN2tzywRAtY6haUpxHPGxNZuyPEgmo+Caw7+BMlIPhipM197+d962i6sGZLylBtCbA==", + "dev": true + }, "phone": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/phone/-/phone-2.4.2.tgz", diff --git a/package.json b/package.json index 9e8ef7ccc4137..74c97d948a7f4 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,7 @@ "npm-merge-driver": "2.3.5", "npm-package-json-lint": "4.6.0", "npm-run-all": "4.1.5", + "phoenix": "1.4.16", "postcss-cli": "6.1.3", "prettier": "npm:wp-prettier@1.19.1", "qs": "6.9.1",