mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Subscribe to notifications/remove polling
- We want to publish when a notification occurs for a specific user, not have the client poll the backend for ever user every minute. - Co-authored-by: @Tirokk <wolle.huss@pjannto.com>
This commit is contained in:
parent
04f0467d2d
commit
2f43069ea0
@ -1,10 +1,11 @@
|
|||||||
import createServer from './server'
|
import createServer from './server'
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
|
|
||||||
const { app, server, httpServer } = createServer()
|
const { server, httpServer } = createServer()
|
||||||
const url = new URL(CONFIG.GRAPHQL_URI)
|
const url = new URL(CONFIG.GRAPHQL_URI)
|
||||||
httpServer.listen({ port: url.port }, () => {
|
httpServer.listen({ port: url.port }, () => {
|
||||||
/* eslint-disable-next-line no-console */
|
/* eslint-disable-next-line no-console */
|
||||||
console.log(`🚀 Server ready at http://localhost:${url.port}${server.graphqlPath}`)
|
console.log(`🚀 Server ready at http://localhost:${url.port}${server.graphqlPath}`)
|
||||||
|
/* eslint-disable-next-line no-console */
|
||||||
console.log(`🚀 Subscriptions ready at ws://localhost:${url.port}${server.subscriptionsPath}`)
|
console.log(`🚀 Subscriptions ready at ws://localhost:${url.port}${server.subscriptionsPath}`)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||||
import { validateNotifyUsers } from '../validation/validationMiddleware'
|
import { validateNotifyUsers } from '../validation/validationMiddleware'
|
||||||
import { PubSub } from 'apollo-server'
|
import {
|
||||||
|
pubsub,
|
||||||
const pubsub = new PubSub()
|
NOTIFICATION_ADDED,
|
||||||
const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
transformReturnType,
|
||||||
|
} from '../../schema/resolvers/notifications'
|
||||||
|
|
||||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||||
const idsOfUsers = extractMentionedUsers(args.content)
|
const idsOfUsers = extractMentionedUsers(args.content)
|
||||||
@ -56,6 +57,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
WHERE user.id in $idsOfUsers
|
WHERE user.id in $idsOfUsers
|
||||||
AND NOT (user)-[:BLOCKED]-(author)
|
AND NOT (user)-[:BLOCKED]-(author)
|
||||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
|
WITH notification, post AS resource, user
|
||||||
`
|
`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -67,6 +69,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
AND NOT (user)-[:BLOCKED]-(author)
|
AND NOT (user)-[:BLOCKED]-(author)
|
||||||
AND NOT (user)-[:BLOCKED]-(postAuthor)
|
AND NOT (user)-[:BLOCKED]-(postAuthor)
|
||||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||||
|
WITH notification, comment AS resource, user
|
||||||
`
|
`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -78,12 +81,16 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
|
|||||||
WHEN notification.createdAt IS NULL
|
WHEN notification.createdAt IS NULL
|
||||||
THEN notification END ).createdAt = toString(datetime())
|
THEN notification END ).createdAt = toString(datetime())
|
||||||
SET notification.updatedAt = toString(datetime())
|
SET notification.updatedAt = toString(datetime())
|
||||||
RETURN notification
|
RETURN notification, resource, user, labels(resource)[0] AS type
|
||||||
`
|
`
|
||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
const writeTxResultPromise = session.writeTransaction(async transaction => {
|
||||||
const notificationTransactionResponse = await transaction.run(mentionedCypher, { id, idsOfUsers, reason })
|
const notificationTransactionResponse = await transaction.run(mentionedCypher, {
|
||||||
return notificationTransactionResponse.records.map(record => record.get('notification').properties)
|
id,
|
||||||
|
idsOfUsers,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
return notificationTransactionResponse.records.map(transformReturnType)
|
||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const [notification] = await writeTxResultPromise
|
const [notification] = await writeTxResultPromise
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import log from './helpers/databaseLogger'
|
import log from './helpers/databaseLogger'
|
||||||
import { PubSub } from 'apollo-server'
|
import { PubSub } from 'apollo-server'
|
||||||
|
import { withFilter } from 'graphql-subscriptions'
|
||||||
|
|
||||||
const pubsub = new PubSub()
|
export const pubsub = new PubSub()
|
||||||
const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
export const NOTIFICATION_ADDED = 'NOTIFICATION_ADDED'
|
||||||
|
export const transformReturnType = record => {
|
||||||
const resourceTypes = ['Post', 'Comment']
|
|
||||||
|
|
||||||
const transformReturnType = record => {
|
|
||||||
return {
|
return {
|
||||||
...record.get('notification').properties,
|
...record.get('notification').properties,
|
||||||
from: {
|
from: {
|
||||||
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
|
__typename: record.get('type'),
|
||||||
...record.get('resource').properties,
|
...record.get('resource').properties,
|
||||||
},
|
},
|
||||||
to: {
|
to: {
|
||||||
@ -22,7 +20,12 @@ const transformReturnType = record => {
|
|||||||
export default {
|
export default {
|
||||||
Subscription: {
|
Subscription: {
|
||||||
notificationAdded: {
|
notificationAdded: {
|
||||||
subscribe: () => pubsub.asyncIterator([NOTIFICATION_ADDED]),
|
subscribe: withFilter(
|
||||||
|
() => pubsub.asyncIterator(NOTIFICATION_ADDED),
|
||||||
|
(payload, variables) => {
|
||||||
|
return payload.notificationAdded.to.id === variables.userId
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Query: {
|
Query: {
|
||||||
@ -90,7 +93,7 @@ export default {
|
|||||||
`
|
`
|
||||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||||
SET notification.read = TRUE
|
SET notification.read = TRUE
|
||||||
RETURN resource, notification, user
|
RETURN resource, notification, user, labels(resource)[0] AS type
|
||||||
`,
|
`,
|
||||||
{ resourceId: args.id, id: currentUser.id },
|
{ resourceId: args.id, id: currentUser.id },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import uuid from 'uuid/v4'
|
import uuid from 'uuid/v4'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError, PubSub } from 'apollo-server'
|
||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
|
||||||
import { PubSub } from 'apollo-server'
|
|
||||||
|
|
||||||
const pubsub = new PubSub()
|
const pubsub = new PubSub()
|
||||||
const POST_ADDED = 'POST_ADDED'
|
const POST_ADDED = 'POST_ADDED'
|
||||||
|
|
||||||
|
|
||||||
const maintainPinnedPosts = params => {
|
const maintainPinnedPosts = params => {
|
||||||
const pinnedPostFilter = { pinned: true }
|
const pinnedPostFilter = { pinned: true }
|
||||||
if (isEmpty(params.filter)) {
|
if (isEmpty(params.filter)) {
|
||||||
|
|||||||
@ -30,3 +30,7 @@ type Query {
|
|||||||
type Mutation {
|
type Mutation {
|
||||||
markAsRead(id: ID!): NOTIFIED
|
markAsRead(id: ID!): NOTIFIED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
notificationAdded(userId: ID!): NOTIFIED
|
||||||
|
}
|
||||||
@ -245,3 +245,7 @@ type Query {
|
|||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Subscription {
|
||||||
|
postAdded: Post
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +0,0 @@
|
|||||||
type Subscription {
|
|
||||||
postAdded: Post
|
|
||||||
notificationAdded: NOTIFIED
|
|
||||||
}
|
|
||||||
@ -3,7 +3,6 @@ import http from 'http'
|
|||||||
import helmet from 'helmet'
|
import helmet from 'helmet'
|
||||||
import { ApolloServer } from 'apollo-server-express'
|
import { ApolloServer } from 'apollo-server-express'
|
||||||
|
|
||||||
|
|
||||||
import CONFIG from './config'
|
import CONFIG from './config'
|
||||||
import middleware from './middleware'
|
import middleware from './middleware'
|
||||||
import { getNeode, getDriver } from './db/neo4j'
|
import { getNeode, getDriver } from './db/neo4j'
|
||||||
@ -11,23 +10,22 @@ import decode from './jwt/decode'
|
|||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
import webfinger from './activitypub/routes/webfinger'
|
import webfinger from './activitypub/routes/webfinger'
|
||||||
|
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
const neode = getNeode()
|
const neode = getNeode()
|
||||||
|
|
||||||
const getContext = async (req) => {
|
const getContext = async req => {
|
||||||
const user = await decode(driver, req.headers.authorization)
|
const user = await decode(driver, req.headers.authorization)
|
||||||
return {
|
return {
|
||||||
driver,
|
driver,
|
||||||
neode,
|
neode,
|
||||||
user,
|
user,
|
||||||
req,
|
req,
|
||||||
cypherParams: {
|
cypherParams: {
|
||||||
currentUserId: user ? user.id : null,
|
currentUserId: user ? user.id : null,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export const context = async (options) => {
|
export const context = async options => {
|
||||||
const { connection, req } = options
|
const { connection, req } = options
|
||||||
if (connection) {
|
if (connection) {
|
||||||
return connection.context
|
return connection.context
|
||||||
@ -63,9 +61,8 @@ const createServer = options => {
|
|||||||
app.use('/.well-known/', webfinger())
|
app.use('/.well-known/', webfinger())
|
||||||
app.use(express.static('public'))
|
app.use(express.static('public'))
|
||||||
server.applyMiddleware({ app, path: '/' })
|
server.applyMiddleware({ app, path: '/' })
|
||||||
const httpServer = http.createServer(app);
|
const httpServer = http.createServer(app)
|
||||||
server.installSubscriptionHandlers(httpServer);
|
server.installSubscriptionHandlers(httpServer)
|
||||||
|
|
||||||
|
|
||||||
return { server, httpServer, app }
|
return { server, httpServer, app }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,13 +22,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex'
|
||||||
import unionBy from 'lodash/unionBy'
|
import unionBy from 'lodash/unionBy'
|
||||||
import { NOTIFICATIONS_POLL_INTERVAL } from '~/constants/notifications'
|
import { notificationQuery, markAsReadMutation, notificationAdded } from '~/graphql/User'
|
||||||
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
|
||||||
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
import CounterIcon from '~/components/_new/generic/CounterIcon/CounterIcon'
|
||||||
import Dropdown from '~/components/Dropdown'
|
import Dropdown from '~/components/Dropdown'
|
||||||
import NotificationList from '../NotificationList/NotificationList'
|
import NotificationList from '../NotificationList/NotificationList'
|
||||||
import { notificationAdded } from '~/graphql/User'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NotificationMenu',
|
name: 'NotificationMenu',
|
||||||
@ -59,6 +58,9 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
user: 'auth/user',
|
||||||
|
}),
|
||||||
unreadNotificationsCount() {
|
unreadNotificationsCount() {
|
||||||
const result = this.notifications.reduce((count, notification) => {
|
const result = this.notifications.reduce((count, notification) => {
|
||||||
return notification.read ? count : count + 1
|
return notification.read ? count : count + 1
|
||||||
@ -77,17 +79,24 @@ export default {
|
|||||||
orderBy: 'updatedAt_desc',
|
orderBy: 'updatedAt_desc',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// pollInterval: NOTIFICATIONS_POLL_INTERVAL,
|
|
||||||
// update({ notifications }) {
|
|
||||||
// return unionBy(notifications, this.notifications, notification => notification.id).sort(
|
|
||||||
// (a, b) => new Date(b.createdAt) - new Date(a.createdAt),
|
|
||||||
// )
|
|
||||||
// },
|
|
||||||
subscribeToMore: {
|
subscribeToMore: {
|
||||||
document: notificationAdded(),
|
document: notificationAdded(),
|
||||||
|
variables() {
|
||||||
|
return {
|
||||||
|
userId: this.user.id,
|
||||||
|
}
|
||||||
|
},
|
||||||
updateQuery: (previousResult, { subscriptionData }) => {
|
updateQuery: (previousResult, { subscriptionData }) => {
|
||||||
const { data: { notificationAdded: newNotification } } = subscriptionData
|
const {
|
||||||
return { notifications: [newNotification, ...previousResult.notifications] }
|
data: { notificationAdded: newNotification },
|
||||||
|
} = subscriptionData
|
||||||
|
return {
|
||||||
|
notifications: unionBy(
|
||||||
|
[newNotification],
|
||||||
|
previousResult.notifications,
|
||||||
|
notification => notification.id,
|
||||||
|
).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
error(error) {
|
error(error) {
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
export const NOTIFICATIONS_POLL_INTERVAL = 60000
|
|
||||||
@ -70,6 +70,7 @@ export const notificationQuery = i18n => {
|
|||||||
read
|
read
|
||||||
reason
|
reason
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
from {
|
from {
|
||||||
__typename
|
__typename
|
||||||
... on Post {
|
... on Post {
|
||||||
@ -108,6 +109,7 @@ export const markAsReadMutation = i18n => {
|
|||||||
read
|
read
|
||||||
reason
|
reason
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
from {
|
from {
|
||||||
__typename
|
__typename
|
||||||
... on Post {
|
... on Post {
|
||||||
@ -137,12 +139,13 @@ export const notificationAdded = () => {
|
|||||||
${commentFragment}
|
${commentFragment}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
|
|
||||||
subscription notifications {
|
subscription notifications($userId: ID!) {
|
||||||
notificationAdded {
|
notificationAdded(userId: $userId) {
|
||||||
id
|
id
|
||||||
read
|
read
|
||||||
reason
|
reason
|
||||||
createdAt
|
createdAt
|
||||||
|
updatedAt
|
||||||
from {
|
from {
|
||||||
__typename
|
__typename
|
||||||
... on Post {
|
... on Post {
|
||||||
|
|||||||
@ -78,7 +78,6 @@ import gql from 'graphql-tag'
|
|||||||
import {
|
import {
|
||||||
userFragment,
|
userFragment,
|
||||||
postFragment,
|
postFragment,
|
||||||
commentFragment,
|
|
||||||
postCountsFragment,
|
postCountsFragment,
|
||||||
userCountsFragment,
|
userCountsFragment,
|
||||||
locationAndBadgesFragment,
|
locationAndBadgesFragment,
|
||||||
@ -225,30 +224,33 @@ export default {
|
|||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
subscribeToMore: {
|
subscribeToMore: {
|
||||||
document: gql`
|
document: gql`
|
||||||
${userFragment}
|
${userFragment}
|
||||||
${userCountsFragment}
|
${userCountsFragment}
|
||||||
${locationAndBadgesFragment('EN')}
|
${locationAndBadgesFragment('EN')}
|
||||||
${postFragment}
|
${postFragment}
|
||||||
${postCountsFragment}
|
${postCountsFragment}
|
||||||
${tagsCategoriesAndPinnedFragment}
|
${tagsCategoriesAndPinnedFragment}
|
||||||
|
|
||||||
subscription Post {
|
subscription Post {
|
||||||
postAdded {
|
postAdded {
|
||||||
...post
|
...post
|
||||||
...postCounts
|
...postCounts
|
||||||
...tagsCategoriesAndPinned
|
...tagsCategoriesAndPinned
|
||||||
author {
|
author {
|
||||||
...user
|
...user
|
||||||
...userCounts
|
...userCounts
|
||||||
...locationAndBadges
|
...locationAndBadges
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`,
|
`,
|
||||||
updateQuery: (previousResult, { subscriptionData }) => {
|
updateQuery: (previousResult, { subscriptionData }) => {
|
||||||
const { data: { postAdded: newPost } } = subscriptionData
|
const {
|
||||||
|
data: { postAdded: newPost },
|
||||||
|
} = subscriptionData
|
||||||
return { Post: [newPost, ...previousResult.Post] }
|
return { Post: [newPost, ...previousResult.Post] }
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ export default ({ app }) => {
|
|||||||
credentials: true,
|
credentials: true,
|
||||||
tokenName: 'human-connection-token',
|
tokenName: 'human-connection-token',
|
||||||
persisting: false,
|
persisting: false,
|
||||||
websocketsOnly: true,
|
websocketsOnly: false,
|
||||||
cache: new InMemoryCache({ fragmentMatcher }),
|
cache: new InMemoryCache({ fragmentMatcher }),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user