Skip to content

Commit

Permalink
Add real-time comments to Reader full-post view (#40396)
Browse files Browse the repository at this point in the history
* Add Lasagna websocket middleware (first pass)

* Step forward

* Sync to latest changes

* One other file

* Fix topics

* Check in a bunch of stuff

* Remove Presence for now

* Fix logging path

* Remove Presence stragglers

* More Presence stragglers

* More cleanup

* Iterate

* Push connection management back into the middleware

Duh?

* Add Reader full post view state subtree

We need a way to query what is onscreen in a Reader full post view if a user hits a reader link directly. The use case is joining related channel(s) after the websocket connects.

* Use new full view post state logic

* Switch to dynamic load of Phoenix.js

* Additional cleanup

* Remove stray addition

To come in follow up PR

* Add config.isEnabled check back for lasagna

* Add webpackchunkname to dynamic import

* To be handled in a subsequent PR

* Final adjustment

Co-authored-by: undemian <undemian@gmail.com>
  • Loading branch information
Hew and unDemian authored Apr 8, 2020
1 parent e5e7345 commit 5b38c63
Show file tree
Hide file tree
Showing 30 changed files with 471 additions and 9 deletions.
14 changes: 8 additions & 6 deletions client/blocks/reader-full-post/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
Expand All @@ -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 );
Expand All @@ -507,5 +509,5 @@ export default connect(

return props;
},
{ markPostSeen, likePost, unlikePost }
{ markPostSeen, setReaderFullViewPostKey, likePost, unlikePost }
)( FullPostView );
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions client/reader/full-post/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 };
Expand Down Expand Up @@ -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() {
Expand Down
2 changes: 2 additions & 0 deletions client/state/action-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,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';
Expand Down
2 changes: 2 additions & 0 deletions client/state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createStore, applyMiddleware, compose } from 'redux';
/**
* Internal dependencies
*/
import config from 'config';
import initialReducer from './reducer';

/**
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions client/state/lasagna/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const socketConnected = () => ( { type: 'LASAGNA_SOCKET_CONNECTED' } );
export const socketDisconnected = () => ( { type: 'LASAGNA_SOCKET_DISCONNECTED' } );
67 changes: 67 additions & 0 deletions client/state/lasagna/middleware.js
Original file line number Diff line number Diff line change
@@ -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
);
78 changes: 78 additions & 0 deletions client/state/lasagna/private-post-channel/actions-to-events.js
Original file line number Diff line number Diff line change
@@ -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 );
};
30 changes: 30 additions & 0 deletions client/state/lasagna/private-post-channel/events-to-actions.js
Original file line number Diff line number Diff line change
@@ -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,
} )
);
} );
}
76 changes: 76 additions & 0 deletions client/state/lasagna/public-post-channel/actions-to-events.js
Original file line number Diff line number Diff line change
@@ -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 );
};
Loading

0 comments on commit 5b38c63

Please sign in to comment.