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

Notifications Service #696

Open
wants to merge 25 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6eefc25
add migrations scripts for notifications table
Aug 31, 2021
700e44a
add subcribe and unsubscribe routes for collection and entity
Aug 31, 2021
9308089
fix eslint errors
Aug 31, 2021
bea6f7f
install events npm package
Aug 31, 2021
454847b
Add Notifications Service and add event listeners
Aug 31, 2021
ee6c17c
Fix code style issues with ESLint
lint-action Aug 31, 2021
bc8c9ad
add subscribe/unsubscribe button in the entity pages
Aug 31, 2021
da4a8a6
Fix code style issues with ESLint
lint-action Aug 31, 2021
bcfb0ad
add subscribe/unsubscribe buttons for collection
Sep 1, 2021
8fb59d6
add primary keys for entity_subscription and collection_subscription
Sep 1, 2021
bb7f790
Merge branch 'notification-service' of github.com:bookbrainz/bookbrai…
Sep 1, 2021
daba75e
add GET /editorId/notifications route
Sep 1, 2021
ee041d1
Fix code style issues with ESLint
lint-action Sep 1, 2021
dfb1f54
don't add another notification if similar already exists
Sep 3, 2021
e0132f6
minor changes
Nov 11, 2021
917d5b9
feat(notification): refactor subscribe button
tr1ten Apr 2, 2022
6cc972d
feat(notifications): refactor routes
tr1ten Apr 2, 2022
fd75e84
Merge branch 'master' into notification-service
tr1ten Apr 2, 2022
7c3ba32
feat(notification): update old component usage
tr1ten Apr 2, 2022
baccf88
feat(notifications): added notifications in navbar
tr1ten Apr 3, 2022
7989606
feat(notification):added time ago for notification
tr1ten Apr 4, 2022
44d3e36
feat(notification): minor improvements
tr1ten Apr 5, 2022
f714500
fixing incorrect imports
tr1ten Apr 5, 2022
5b462e3
correctly importing config file
tr1ten Apr 5, 2022
87e4a18
wrap message handler with try-catch
tr1ten May 21, 2022
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"cross-env": "^5.1.1",
"date-fns": "^2.15.0",
"debug": "^3.2.7",
"events": "^3.3.0",
"express": "^4.17.1",
"express-session": "^1.17.1",
"express-slow-down": "^1.3.1",
Expand Down
7 changes: 7 additions & 0 deletions sql/migrations/2021-09-notification-service/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
BEGIN;

DROP TABLE IF EXISTS bookbrainz.notification;
DROP TABLE IF EXISTS bookbrainz.entity_subscription;
DROP TABLE IF EXISTS bookbrainz.collection_subscription;

COMMIT;
36 changes: 36 additions & 0 deletions sql/migrations/2021-09-notification-service/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
BEGIN;

CREATE TABLE IF NOT EXISTS bookbrainz.notification (
id UUID PRIMARY KEY DEFAULT public.uuid_generate_v4(),
subscriber_id INT NOT NULL,
read BOOLEAN NOT NULL DEFAULT FALSE,
notification_text TEXT NOT NULL CHECK (notification_text <> ''),
notification_redirect_link TEXT NOT NULL CHECK (notification_redirect_link <> ''),
timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT timezone('UTC'::TEXT, now())
);
ALTER TABLE bookbrainz.notification ADD FOREIGN KEY (subscriber_id) REFERENCES bookbrainz.editor (id);

CREATE TABLE bookbrainz.entity_subscription (
bbid UUID,
subscriber_id INT,
PRIMARY KEY (
bbid,
subscriber_id
)
);
ALTER TABLE bookbrainz.entity_subscription ADD FOREIGN KEY (bbid) REFERENCES bookbrainz.entity (bbid);
ALTER TABLE bookbrainz.entity_subscription ADD FOREIGN KEY (subscriber_id) REFERENCES bookbrainz.editor (id);


CREATE TABLE bookbrainz.collection_subscription (
collection_id UUID,
subscriber_id INT,
PRIMARY KEY (
collection_id,
subscriber_id
)
);
ALTER TABLE bookbrainz.collection_subscription ADD FOREIGN KEY (collection_id) REFERENCES bookbrainz.user_collection (id);
ALTER TABLE bookbrainz.collection_subscription ADD FOREIGN KEY (subscriber_id) REFERENCES bookbrainz.editor (id);

COMMIT;
85 changes: 85 additions & 0 deletions src/client/components/pages/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ class CollectionPage extends React.Component {
super(props);
this.state = {
entities: this.props.entities,
isSubscribed: false,
message: {
text: null,
type: null
Expand All @@ -138,13 +139,20 @@ class CollectionPage extends React.Component {
this.handleRemoveEntities = this.handleRemoveEntities.bind(this);
this.handleShowDeleteModal = this.handleShowDeleteModal.bind(this);
this.handleCloseDeleteModal = this.handleCloseDeleteModal.bind(this);
this.handleSubscribe = this.handleSubscribe.bind(this);
this.handleShowAddEntityModal = this.handleShowAddEntityModal.bind(this);
this.handleCloseAddEntityModal = this.handleCloseAddEntityModal.bind(this);
this.handleAlertDismiss = this.handleAlertDismiss.bind(this);
this.handleUnsubscribe = this.handleUnsubscribe.bind(this);
this.searchResultsCallback = this.searchResultsCallback.bind(this);
this.setIsSubscribed = this.setIsSubscribed.bind(this);
this.closeAddEntityModalShowMessageAndRefreshTable = this.closeAddEntityModalShowMessageAndRefreshTable.bind(this);
}

async componentDidMount() {
await this.setIsSubscribed();
}

searchResultsCallback(newResults) {
this.setState({entities: newResults});
}
Expand Down Expand Up @@ -224,6 +232,59 @@ class CollectionPage extends React.Component {
}, this.pagerElementRef.triggerSearch);
}

handleSubscribe() {
Copy link
Member

Choose a reason for hiding this comment

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

As far as I can tell, all this code (handleSubscribe, handleUnsubscribe, setIsSubscribed and the buttons that go with it) is duplicated for each display page. Instead it would be better if the whole subscription part was a separate component that can then be added to the relevant pages.
You'll probably want to pass the submissionUrl as a prop to make it more flexible.

We might end up with separate CollectionSubscription and an EntitySubscription components if they require very different implementations.

const submissionUrl = '/subscription/subscribe/collection';
const collectionId = this.props.collection.id;
const subscriberId = this.props.userId;
Copy link
Member

Choose a reason for hiding this comment

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

Would this not be accessible in the route handler on the server?
How come you don't need to send the userId for the entity subscribe routes but you do send them for the collections subscribe route?

request.post(submissionUrl)
.send({collectionId, subscriberId})
.then((res) => {
const {isSubscribed} = res.body;
this.setState({isSubscribed});
}, () => {
this.setState({
message: {
text: 'Something went wrong! Please try again later',
type: 'danger'
}
});
});
}

handleUnsubscribe() {
const submissionUrl = '/subscription/unsubscribe/collection';
const collectionId = this.props.collection.id;
request.post(submissionUrl)
.send({collectionId})
.then((res) => {
const {isSubscribed} = res.body;
this.setState({isSubscribed});
}, () => {
this.setState({
message: {
text: 'Something went wrong! Please try again later',
type: 'danger'
}
});
});
}

setIsSubscribed() {
const url = `/subscription/collection/isSubscribed/${this.props.collection.id}`;
request.get(url)
.then(res => {
const {isSubscribed} = res.body;
this.setState({isSubscribed});
}, () => {
this.setState({
message: {
text: 'Something went wrong! Please try again later',
type: 'danger'
}
});
});
}

render() {
const messageComponent = this.state.message.text ? <Alert bsStyle={this.state.message.type} className="margin-top-1" onDismiss={this.handleAlertDismiss}>{this.state.message.text}</Alert> : null;
const EntityTable = getEntityTable(this.props.collection.entityType);
Expand Down Expand Up @@ -262,6 +323,30 @@ class CollectionPage extends React.Component {
<CollectionAttributes collection={this.props.collection}/>
</Col>
</Row>
{
!this.state.isSubscribed &&
<Button
bsSize="small"
bsStyle="success"
className="margin-bottom-d5"
title="Subscribe"
onClick={this.handleSubscribe}
>
Subscribe
</Button>
}
{
this.state.isSubscribed &&
<Button
bsSize="small"
bsStyle="danger"
className="margin-bottom-d5"
title="Unsubscribe"
onClick={this.handleUnsubscribe}
>
Unsubscribe
</Button>
}
<EntityTable{...propsForTable}/>
{messageComponent}
<div className="margin-top-1 text-left">
Expand Down
56 changes: 54 additions & 2 deletions src/client/components/pages/entities/author.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import * as bootstrap from 'react-bootstrap';
import * as entityHelper from '../../../helpers/entity';

import React, {useEffect, useState} from 'react';
import EntityAnnotation from './annotation';
import EntityFooter from './footer';
import EntityImage from './image';
Expand All @@ -27,10 +27,10 @@ import EntityRelatedCollections from './related-collections';
import EntityTitle from './title';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import React from 'react';
import {kebabCase as _kebabCase} from 'lodash';
import {faPlus} from '@fortawesome/free-solid-svg-icons';
import {labelsForAuthor} from '../../../helpers/utils';
import request from 'superagent';


const {deletedEntityMessage, extractAttribute, getTypeAttribute, getEntityUrl,
Expand Down Expand Up @@ -109,6 +109,38 @@ AuthorAttributes.propTypes = {

function AuthorDisplayPage({entity, identifierTypes, user}) {
const urlPrefix = getEntityUrl(entity);
const [isSubscribed, setIsSubscribed] = useState(false);
useEffect(() => {
request.get(`/subscription/entity/isSubscribed/${entity.bbid}`).then(response => {
if (response.body.isSubscribed) {
setIsSubscribed(true);
}
});
});
function handleUnsubscribe(bbid) {
const submissionUrl = '/subscription/unsubscribe/entity';
request.post(submissionUrl)
.send({bbid})
.then((res) => {
setIsSubscribed(false);
}, (error) => {
// eslint-disable-next-line no-console
console.log('error thrown');
});
}
function handleSubscribe(bbid) {
const submissionUrl = '/subscription/subscribe/entity';
Copy link
Member

Choose a reason for hiding this comment

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

I think this URL structure would be more appropriate and "RESTy":
/${entityType}/${bbid}/subscribe and associated /unsubscribe and /subscribed
I guess that means we don't need to send any information with that request anymore: BBID is in the route and the user needs to be logged in to access the route so we have their info in the route handler.

That would also work for collections: /collection/${collectionId}/subscribe

That means a fair amount of refactoring on the routes, I know, sorry about that :/
I think you'll end up with a middleware or two (one for entities, one for collections) which should be reusable for each entity's routes. In theory, in these routes you should have access to the user information

request.post(submissionUrl)
.send({bbid})
.then((res) => {
setIsSubscribed(true);
}, (error) => {
// eslint-disable-next-line no-console
console.log('error thrown');
});
}

/* eslint-disable react/jsx-no-bind */
return (
<div>
<Row className="entity-display-background">
Expand All @@ -124,6 +156,26 @@ function AuthorDisplayPage({entity, identifierTypes, user}) {
<AuthorAttributes author={entity}/>
</Col>
</Row>
{
!isSubscribed &&
<Button
bsStyle="success"
className="margin-top-d15"
onClick={() => handleSubscribe(entity.bbid)}
>
Subscribe
</Button>
}
{
isSubscribed &&
<Button
bsStyle="danger"
className="margin-top-d15"
onClick={() => handleUnsubscribe(entity.bbid)}
>
Unsubscribe
</Button>
}
<EntityAnnotation entity={entity}/>
{!entity.deleted &&
<React.Fragment>
Expand Down
57 changes: 55 additions & 2 deletions src/client/components/pages/entities/edition-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import * as bootstrap from 'react-bootstrap';
import * as entityHelper from '../../../helpers/entity';
import React, {useEffect, useState} from 'react';
import EditionTable from './edition-table';
import EntityAnnotation from './annotation';
import EntityFooter from './footer';
Expand All @@ -26,11 +27,11 @@ import EntityLinks from './links';
import EntityRelatedCollections from './related-collections';
import EntityTitle from './title';
import PropTypes from 'prop-types';
import React from 'react';
import request from 'superagent';


const {deletedEntityMessage, getTypeAttribute, getEntityUrl, ENTITY_TYPE_ICONS, getSortNameOfDefaultAlias} = entityHelper;
const {Col, Row} = bootstrap;
const {Col, Row, Button} = bootstrap;

function EditionGroupAttributes({editionGroup}) {
if (editionGroup.deleted) {
Expand Down Expand Up @@ -65,6 +66,38 @@ EditionGroupAttributes.propTypes = {

function EditionGroupDisplayPage({entity, identifierTypes, user}) {
const urlPrefix = getEntityUrl(entity);
const [isSubscribed, setIsSubscribed] = useState(false);
useEffect(() => {
request.get(`/subscription/entity/isSubscribed/${entity.bbid}`).then(response => {
if (response.body.isSubscribed) {
setIsSubscribed(true);
}
});
});
function handleUnsubscribe(bbid) {
const submissionUrl = '/subscription/unsubscribe/entity';
request.post(submissionUrl)
.send({bbid})
.then((res) => {
setIsSubscribed(false);
}, (error) => {
// eslint-disable-next-line no-console
console.log('error thrown');
});
}
function handleSubscribe(bbid) {
const submissionUrl = '/subscription/subscribe/entity';
request.post(submissionUrl)
.send({bbid})
.then((res) => {
setIsSubscribed(true);
}, (error) => {
// eslint-disable-next-line no-console
console.log('error thrown');
});
}

/* eslint-disable react/jsx-no-bind */
return (
<div>
<Row className="entity-display-background">
Expand All @@ -80,6 +113,26 @@ function EditionGroupDisplayPage({entity, identifierTypes, user}) {
<EditionGroupAttributes editionGroup={entity}/>
</Col>
</Row>
{
!isSubscribed &&
<Button
bsStyle="success"
className="margin-top-d15"
onClick={() => handleSubscribe(entity.bbid)}
>
Subscribe
</Button>
}
{
isSubscribed &&
<Button
bsStyle="danger"
className="margin-top-d15"
onClick={() => handleUnsubscribe(entity.bbid)}
>
Unsubscribe
</Button>
}
<EntityAnnotation entity={entity}/>
{!entity.deleted &&
<React.Fragment>
Expand Down
Loading