Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add real-time comments to Reader full-post view #40396

Merged
merged 29 commits into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5a63490
Add Lasagna websocket middleware (first pass)
mhsdef Mar 24, 2020
74f6fa3
Step forward
mhsdef Mar 25, 2020
3a41869
Sync to latest changes
mhsdef Mar 29, 2020
be2ee42
One other file
mhsdef Mar 29, 2020
c8f2ce2
Fix topics
mhsdef Mar 29, 2020
d8f2713
Check in a bunch of stuff
mhsdef Mar 31, 2020
f0e7ee9
Remove Presence for now
mhsdef Apr 1, 2020
e511e86
Fix logging path
mhsdef Apr 1, 2020
f496ca8
Remove Presence stragglers
mhsdef Apr 1, 2020
d02422c
More Presence stragglers
mhsdef Apr 1, 2020
6e105c6
More cleanup
mhsdef Apr 1, 2020
d4324ba
Merge branch 'master' into try/realtime-comments
mhsdef Apr 1, 2020
6e23185
Iterate
mhsdef Apr 2, 2020
fa97009
Push connection management back into the middleware
mhsdef Apr 2, 2020
f6ef74e
Add Reader full post view state subtree
mhsdef Apr 3, 2020
9bbae1b
Use new full view post state logic
mhsdef Apr 3, 2020
87b0a86
Switch to dynamic load of Phoenix.js
mhsdef Apr 3, 2020
2c35393
Additional cleanup
mhsdef Apr 3, 2020
ed5149e
Merge branch 'master' into try/realtime-comments
unDemian Apr 6, 2020
aebbe1b
Merge branch 'master' into try/realtime-comments
mhsdef Apr 6, 2020
dde8e3b
Remove stray addition
mhsdef Apr 6, 2020
76a83fa
Add config.isEnabled check back for lasagna
mhsdef Apr 6, 2020
5790016
Merge branch 'master' into try/realtime-comments
unDemian Apr 7, 2020
3be94fe
Merge branch 'master' into try/realtime-comments
mhsdef Apr 7, 2020
7277061
Add webpackchunkname to dynamic import
mhsdef Apr 7, 2020
be75f4f
To be handled in a subsequent PR
mhsdef Apr 7, 2020
0a3aa22
Merge branch 'master' into try/realtime-comments
unDemian Apr 8, 2020
5b82179
Final adjustment
mhsdef Apr 8, 2020
19b1dae
Merge branch 'master' into try/realtime-comments
mhsdef Apr 8, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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';
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' } );
Comment on lines +1 to +2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The action types are added to client/state/action-types but plain strings are used here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should be fixed in the next PR #40720

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