diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 1f636d981..717926413 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -131,6 +131,7 @@ Factory.define('post') imageBlurred: false, imageAspectRatio: 1.333, clickedCount: 0, + viewedTeaserCount: 0, }) .attr('pinned', ['pinned'], (pinned) => { // Convert false to null diff --git a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.js b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.js new file mode 100644 index 000000000..ee1fad124 --- /dev/null +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.js @@ -0,0 +1,53 @@ +import { getDriver } from '../../db/neo4j' + +export const description = ` +This migration adds the viewedTeaserCount property to all posts, setting it to 0. +` + +module.exports.up = async function (next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + SET p.viewedTeaserCount = 0 + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} + +module.exports.down = async function (next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + try { + // Implement your migration here. + await transaction.run(` + MATCH (p:Post) + REMOVE p.viewedTeaserCount + `) + await transaction.commit() + next() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + throw new Error(error) + } finally { + session.close() + } +} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index fe201e4cb..7aeb7252a 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -168,6 +168,7 @@ export default shield( UpdateDonations: isAdmin, GenerateInviteCode: isAuthenticated, switchUserRole: isAdmin, + markTeaserAsViewed: allow, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/Post.js b/backend/src/models/Post.js index 42bf844e1..235e7d68d 100644 --- a/backend/src/models/Post.js +++ b/backend/src/models/Post.js @@ -23,6 +23,7 @@ export default { deleted: { type: 'boolean', default: false }, disabled: { type: 'boolean', default: false }, clickedCount: { type: 'int', default: 0 }, + viewedTeaserCount: { type: 'int', default: 0 }, notified: { type: 'relationship', relationship: 'NOTIFIED', diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 47e150cac..d5d175fc0 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -89,6 +89,7 @@ export default { SET post.createdAt = toString(datetime()) SET post.updatedAt = toString(datetime()) SET post.clickedCount = 0 + SET post.viewedTeaserCount = 0 WITH post MATCH (author:User {id: $userId}) MERGE (post)<-[:WROTE]-(author) @@ -316,6 +317,30 @@ export default { } return unpinnedPost }, + markTeaserAsViewed: async (_parent, params, context, _resolveInfo) => { + const session = context.driver.session() + const writeTxResultPromise = session.writeTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (post:Post { id: $params.id }) + MATCH (user:User { id: $userId }) + MERGE (user)-[relation:VIEWED_TEASER { }]->(post) + ON CREATE + SET relation.createdAt = toString(datetime()), + post.viewedTeaserCount = post.viewedTeaserCount + 1 + RETURN post + `, + { userId: context.user.id, params }, + ) + return transactionResponse.records.map((record) => record.get('post').properties) + }) + try { + const [post] = await writeTxResultPromise + return post + } finally { + session.close() + } + }, }, Post: { ...Resolver('Post', { @@ -342,6 +367,8 @@ export default { boolean: { shoutedByCurrentUser: 'MATCH(this)<-[:SHOUTED]-(related:User {id: $cypherParams.currentUserId}) RETURN COUNT(related) >= 1', + viewedTeaserByCurrentUser: + 'MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1', }, }), relatedContributions: async (parent, params, context, resolveInfo) => { diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 934883b12..898b7659f 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -40,6 +40,7 @@ const searchPostsSetup = { commentsCount: toString(size(comments)), shoutedCount: toString(size(shouter)), clickedCount: toString(resource.clickedCount) + viewedTeaserCount: toString(resource.viewedTeaserCount) }`, limit: 'LIMIT $limit', } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 99e1e9e59..2f9221dd1 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -158,6 +158,12 @@ type Post { clickedCount: Int! + viewedTeaserCount: Int! + viewedTeaserByCurrentUser: Boolean! + @cypher( + statement: "MATCH (this)<-[:VIEWED_TEASER]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1" + ) + emotions: [EMOTED] emotionsCount: Int! @cypher(statement: "MATCH (this)<-[emoted:EMOTED]-(:User) RETURN COUNT(DISTINCT emoted)") @@ -195,6 +201,7 @@ type Mutation { RemovePostEmotions(to: _PostInput!, data: _EMOTEDInput!): EMOTED pinPost(id: ID!): Post unpinPost(id: ID!): Post + markTeaserAsViewed(id: ID!): Post } type Query { diff --git a/webapp/components/PostTeaser/PostTeaser.spec.js b/webapp/components/PostTeaser/PostTeaser.spec.js index 5b71347d0..7a37d710f 100644 --- a/webapp/components/PostTeaser/PostTeaser.spec.js +++ b/webapp/components/PostTeaser/PostTeaser.spec.js @@ -26,6 +26,7 @@ describe('PostTeaser', () => { shoutedCount: 0, commentsCount: 0, clickedCount: 0, + viewedTeaserCount: 0, name: 'It is a post', author: { id: 'u1', diff --git a/webapp/components/PostTeaser/PostTeaser.story.js b/webapp/components/PostTeaser/PostTeaser.story.js index 41c34cad4..c82396101 100644 --- a/webapp/components/PostTeaser/PostTeaser.story.js +++ b/webapp/components/PostTeaser/PostTeaser.story.js @@ -44,6 +44,7 @@ export const post = { commentsCount: 12, categories: [], shoutedCount: 421, + viewedTeaserCount: 1584, __typename: 'Post', } diff --git a/webapp/components/PostTeaser/PostTeaser.vue b/webapp/components/PostTeaser/PostTeaser.vue index 2c475c15a..0efb8528f 100644 --- a/webapp/components/PostTeaser/PostTeaser.vue +++ b/webapp/components/PostTeaser/PostTeaser.vue @@ -6,9 +6,9 @@ @@ -39,6 +39,11 @@ icon="hand-pointer" :count="post.clickedCount" :title="$t('contribution.amount-clicks', { amount: post.clickedCount })" + +